A Qdrant collection snapshot is a tar archive of one collection on one node at one instant, holding the collection configuration, all points and payloads, and no collection aliases, so a distributed cluster needs one snapshot per node. Restores go through three paths, and the priority parameter that decides whether the snapshot wins sits in the JSON body on PUT /collections/{name}/snapshots/recover but in the query string on POST /collections/{name}/snapshots/upload. The documentation lists three values, replica as the default, snapshot and no_sync, and states that with replica priority you end up with an empty collection because the cluster copy was preferred. Reading v1.19.0, the enum in lib/collection/src/operations/snapshot_ops.rs carries a default attribute on Snapshot and the recover handler resolves the field with unwrap_or_default(), so the documented default and the source default disagree and the only safe move is to set priority explicitly on every restore. The parameter is consulted only when the recovered shard has another Active replica on a different peer: with no such replica Qdrant activates the shard and the snapshot data lands whatever you set. The start-up path is single node only, refuses an existing target collection and names --force-snapshot in its own panic message, and a snapshot restores only to the same minor version or the next one. milvus-backup v0.5.16 publishes a static matrix instead, backups from Milvus 2.2 and up restoring to 2.4 and up and never to an older version, and every backup it takes is a full copy. Restore into a scratch collection this week, run POST /collections/{name}/points/count with exact set to true, and read GET /collections/{name}/cluster to confirm every replica is Active.
A Qdrant snapshot restore can return success, create the collection, and leave you with zero points. That is not a broken restore. It is documented behaviour, and Qdrant's snapshots page states it under the heading Snapshot priority: "To recover a new collection from a snapshot, you need to set the priority to snapshot. With snapshot priority, all data from the snapshot will be recovered onto the cluster. With replica priority (default), you'd end up with an empty collection because the collection on the cluster did not contain any points and that source was preferred."
This is an empty collection, not an empty result set. If your queries come back with nothing against a collection that you know holds points, the causes sit on the query side and we worked through the query-side reasons a search comes back empty separately. What follows is about the restore itself.
Everything below was read against Qdrant v1.19.0, released 5 August 2026, and against the snapshots documentation page. Three things there change a restore runbook: priority is consulted only under a precondition the source makes explicit, the parameter moves between the request body and the query string depending on which REST endpoint you call, and the flag that overwrites a collection at start-up is spelled one way in the docs and another in Qdrant's own panic message.
What a Qdrant snapshot actually contains
The definition carries three qualifiers, and each one constrains your recovery plan. From the snapshots page, available as of v0.8.4: "Snapshots are tar archive files that contain data and configuration of a specific collection on a specific node at a specific time."
One collection. Not the instance, not a set of collections, not aliases: "A collection level snapshot only contains data within that collection, including the collection configuration, all points and payloads. Collection aliases are not included and can be migrated or recovered separately." If your retrieval service queries documents_current and that name is an alias pointing at documents_v7, the restore brings back the points and none of the routing.
One node. The same page says you "must create snapshots for each node separately when dealing with a single collection", and repeats it as an aside: "A single snapshot will contain only the data stored on the node on which the snapshot was created." A three node cluster with four collections produces twelve archives per restore point, and a backup job that calls one node on a cron schedule captures that node's share and reports green.
One instant. Creation is documented as a synchronous operation producing a tar archive, so everything written after the call is a gap you close from your ingestion pipeline rather than from the archive.
# Create a snapshot of one collection on THIS node curl -X POST 'http://localhost:6333/collections/orders/snapshots?wait=true' \ -H 'api-key: ********' # List what this node holds curl -X GET 'http://localhost:6333/collections/orders/snapshots' \ -H 'api-key: ********'
Run that against every node, not against a load balancer in front of them. Where the archives land is worth deciding before you need them, because the default is a directory on the same disk as the data you are protecting.
storage: # Where snapshots land. Default ./snapshots, or /qdrant/snapshots in the Docker image. snapshots_path: /mnt/snapshots # Temporary files during snapshot creation. Available as of v1.3.0. temp_path: /mnt/scratch
Both keys have environment equivalents, QDRANT__STORAGE__SNAPSHOTS_PATH among them, which is the form to use under an orchestrator, and since v1.10.0 there is an S3 backend under storage.snapshots_config.snapshots_storage. Wherever it lands, the archive is a second full copy of the corpus with a different access model than the query API, which is why who is allowed to create and download those snapshots is a separate review.
There is a second artefact type, and the difference decides whether aliases are in your backup at all.
The alias row bites in production: the collection snapshot is the artefact a distributed cluster can produce, and it is the one without aliases in it.
| Collection snapshot | Full storage snapshot | |
|---|---|---|
| Available since | v0.8.4 | v0.8.5 |
| Contains | Collection config, all points and payloads | The whole storage, including collection aliases |
| Collection aliases | Not included, migrated or recovered separately | Included |
| Distributed mode | Supported, one snapshot per node per collection | Not supported, single node only |
| Create endpoint | POST /collections/{name}/snapshots | POST /snapshots |
| Restore | REST recover or upload, or CLI at start-up | CLI at start-up only, ./qdrant --storage-snapshot FILE |
| Qdrant Cloud | Restore supported via file URI or upload | Can be created and downloaded, cannot restore a Cloud cluster |
The priority parameter is the whole restore
Qdrant frames priority as conflict resolution: "When recovering a snapshot to a non-empty node, there may be conflicts between the snapshot data and the existing data", with the note that "The default priority may not be best for all situations." The three values, quoted whole: "replica: (default) prefer existing data over the snapshot", "snapshot: prefer snapshot data over existing data", "no_sync: restore snapshot without any additional synchronization."
The third carries a warning to read as written rather than softened. no_sync "is for specialized use cases and is not commonly used. It allows managing shards and transferring shards between clusters manually without any additional synchronization. Using it incorrectly will leave your cluster in a broken state."
The table adds the fourth case: leaving the parameter out.
The last row is the honest one. The documentation says the default is replica. The source says otherwise: at v1.19.0 SnapshotPriority derives Default with the #[default] attribute on the Snapshot variant, and lib/storage/src/content_manager/snapshots/recover.rs resolves the field with let priority = priority.unwrap_or_default();. The three shard-snapshot handlers in src/actix/api/snapshot_api.rs do the same, and the enum default is identical at every minor release tag from v1.12.0 through v1.19.0. We did not run a binary to see which reading governs yours, so we will not adjudicate it. Both support one conclusion: set priority explicitly on every restore.
| priority | Documented meaning | When it is consulted | What Qdrant does at v1.19.0 | End state on a freshly created empty collection |
|---|---|---|---|---|
replica (documented default) | Prefer existing data over the snapshot | Only when the shard has another Active replica on a different peer | Requests a shard transfer from that other replica onto the restored node | Empty collection, the docs' own worked example |
snapshot | Prefer snapshot data over existing data | Only when the shard has another Active replica on a different peer | Activates the restored shard, removes the other replicas beyond replication_factor minus 1 and marks every remaining one Dead | Snapshot data, other replicas rebuilt from it |
no_sync | Restore snapshot without any additional synchronization | Only when the shard has another Active replica on a different peer | Activates the restored shard and performs no synchronization at all | Divergent replicas, and the docs say misusing it "will leave your cluster in a broken state" |
| not set | The OpenAPI spec declares the property default as null | Same precondition | Resolved with unwrap_or_default(), and the enum's default variant is Snapshot in lib/collection/src/operations/snapshot_ops.rs | Depends which reading holds, which is the reason to set it |
Why the empty collection happens, in code
The precondition turns this from superstition into mechanism. In recover.rs at v1.19.0, before the priority match runs, Qdrant builds other_active_replicas: replicas of this shard on peers other than this one whose state is Active or ReshardingScaleDown. If that list is empty it calls activate_shard(...), commented as there being no other active replicas so no de-sync is possible. The priority match sits in the else branch.
On a single node instance, or on any shard whose only replica is the one you just restored, priority never comes into play and the snapshot data lands whatever you passed. Setting priority=snapshot there is a correct habit but it is not what makes the restore work, and a single node restore that came back empty has a cause elsewhere: the endpoint, the collection name, or the archive you pointed at.
When there is another active replica, each arm does something specific.
The Replica arm takes the first other active replica and calls toc.request_shard_transfer(...) from that peer onto this one, commented as the replica being the source of truth the recovered data must sync with. That transfer replaces what you just unpacked, and if you created the target collection moments earlier so the restore had somewhere to go, it faithfully brings you zero points.
The Snapshot arm activates the restored shard, then computes replicas_to_keep as replication_factor - 1 and replicas_to_remove as other_active_replicas.len().saturating_sub(replicas_to_keep), issues request_remove_replica for that many peers and marks the rest Dead. Snapshot priority is not a passive preference: it removes or kills other replicas so the cluster converges on the copy you restored. Correct for a migration, destructive if you reached for it to repair one sick node.
The NoSync arm activates the shard and does nothing else, which is the whole of the warning quoted above.
# Qdrant names the archive <collection>-<peer id>-<timestamp>.snapshot
# and writes it under <snapshots_path>/<collection>/
curl -X PUT 'http://localhost:6333/collections/orders/snapshots/recover?wait=true' \
-H 'api-key: ********' \
-H 'Content-Type: application/json' \
-d '{
"location": "file:///qdrant/snapshots/orders/orders-3766212330831337-2026-08-30-09-12-04.snapshot",
"priority": "snapshot"
}'Priority is not the only route to a collection with no points. An issue filed in August 2026 and still open reports that restoring a snapshot of a collection whose sharding_method is custom into a new collection name through the upload endpoint produces a target with correct configuration and no points, and that the target may hold only metadata files such as config.json, shard_key_mapping.json and version.info on disk. If you use custom sharding, restore into a scratch collection and count points before trusting that path.
Three restore paths and where priority lives in each
The docs name three: recovery from a URL or local file, recovery from an uploaded file, and recovery during start-up. They are not interchangeable. The parameter you cannot afford to omit sits in a different place in each of the two REST paths, and does not exist on the third.
The first two rows differ in one detail that decides whether your priority setting is read at all. In the OpenAPI spec at v1.19.0, PUT /collections/{collection_name}/snapshots/recover declares only collection_name as a path parameter and wait as a query parameter; priority is a field on the SnapshotRecover body schema, next to location. On POST /collections/{collection_name}/snapshots/upload it is a query parameter alongside wait and checksum. Append ?priority=snapshot to the recover endpoint and nothing rejects it: the field is absent from the body and you get default behaviour.
That body schema carries two more fields worth wiring in. checksum takes an optional SHA256 hash Qdrant validates before recovery, which catches an archive truncated in transit. api_key is the credential used when fetching from a remote URL, so a URL restore between two secured clusters needs no opening up of the source.
One interface does not exist: at v1.19.0 the public Snapshots gRPC service exposes Create, List, Delete and their full-storage equivalents, and no recover RPC. Recovery is REST only, so a gRPC control plane has one exception in it.
curl -X POST 'http://{qdrant-url}:6333/collections/{collection_name}/snapshots/upload?priority=snapshot' \
-H 'api-key: ********' \
-H 'Content-Type:multipart/form-data' \
-F 'snapshot=@/path/to/snapshot-2022-10-10.snapshot'That is the docs' own upload example, placeholders included.
The deployment boundary decides which paths you have
Qdrant Cloud removes options rather than restricting who may use them: "In Qdrant Cloud, restoring via URL is not supported since all outbound traffic is blocked for security purposes. You may still restore via file URI or via an uploaded file." Start-up recovery "cannot be used in a multi-node deployment and cannot be used in Qdrant Cloud". A full storage snapshot can be created and downloaded from Cloud but cannot restore a Cloud cluster, which is why the same page steers Cloud users to disk-level Backups for disaster recovery. Inside your own perimeter the situation inverts. The start-up path and file-URI recovery are the two you can actually rehearse, because the archive never leaves the network and the restore does not depend on an egress rule somebody has to approve. For a regulated estate that is a real advantage with a real obligation attached, because the recovery procedure is now yours to prove rather than a line in a provider's service description. The auditable version of that proof is a dated record from a scratch collection: the exact point count, the per-shard replica states, the configuration showing the index parameters came back, and a retrieval eval run against the restored data.
| Path | Interface | Where priority goes | Multi-node | Qdrant Cloud | Creates a missing collection | Behaviour if the collection exists |
|---|---|---|---|---|---|---|
| URL or local file | PUT /collections/{name}/snapshots/recover | JSON request body | Yes | File URI yes, URL no, outbound traffic blocked | Yes | Overwrites this node's data for the collection |
| Uploaded file | POST /collections/{name}/snapshots/upload | Query parameter | Yes | Yes | Yes | Overwrites this node's data for the collection |
| Start-up | ./qdrant --snapshot FILE:COLLECTION | Not available | No | No | Yes | Exits with an error unless --force-snapshot is passed |
The start-up path fails loudly, and that is the good case
The one restore that cannot silently drop your data is the one you can use only on a single node. It goes through the CLI: --snapshot "accepts a list of pairs such as <snapshot_file_path>:<target_collection_name>", and "The target collection must be absent otherwise the program will exit with an error."
# Single node only. Target collection must not already exist. ./qdrant --snapshot /snapshots/orders-2026-08-30.snapshot:orders # Same, but overwrite an existing collection ./qdrant --force-snapshot --snapshot /snapshots/orders-2026-08-30.snapshot:orders # Whole-storage restore, also single node only ./qdrant --storage-snapshot /snapshots/full-snapshot-2026-08-30-11-20-51.snapshot
Run the first command against a storage directory that already holds a collection named orders and v1.19.0 panics in src/snapshots.rs with the exact text Collection orders already exists. Use --force-snapshot to overwrite it. The whole-storage restore raises a parallel message for an existing alias. That string is also the authority on the spelling: the documentation page writes the flag with an underscore, while the CLI struct in src/main.rs declares the field as force_snapshot with no case override, so the derived long flag is kebab-case. Use the hyphen.
The parser checks three things about each pair. Collection name is missing: {value} fires when there is no colon or when the part after it is empty, so --snapshot /snapshots/orders-2026-08-30.snapshot with no target name prints Collection name is missing: /snapshots/orders-2026-08-30.snapshot. Snapshot path is missing: {value} fires when the part before the colon is empty. Collection name must not contain slashes: {value} fires on a name with a path separator. The pair splits with rsplit_once(':'), on the last colon, so a path containing a colon parses and a collection name containing one never will.
The REST paths say the opposite in their own OpenAPI descriptions: "This will overwrite any data, stored on this node, for the collection. If collection does not exist - it will be created." No confirmation, no refusal, no flag. The strict path is the one unavailable exactly where clusters live.
The version window turns old backups into an upgrade question
Qdrant states the compatibility rule with a worked example: "Snapshots generated in one Qdrant cluster can only be restored to other Qdrant clusters running the same minor version or the next minor version. For instance, a snapshot captured from a v1.18.1 cluster can only be restored to clusters running version v1.18.x, where x is equal to or greater than 1, or v1.19.x."
The archive knows where it came from. At v1.19.0, snapshot creation writes CollectionVersion::current_raw() into the storage version file inside the tar, and that value is the crate version. Restore does not rewrite it, so the producing version travels with the artefact.
That window moves. v1.18.0 shipped 11 May 2026, v1.18.1 on 22 May, v1.18.2 on 4 June, v1.18.3 on 17 July and v1.19.0 on 5 August 2026. A snapshot taken against v1.18.1 is inside the documented window for a v1.19.x cluster and outside it once your estate reaches v1.20. Nothing about the archive changes that day; your ability to restore it does. Long-horizon retention against that cadence is an upgrade-path question: either keep a matching binary for every archive you intend to restore, or accept that older archives are inputs to a migration rather than backups.
The downgrade direction is a hard stop. At v1.19.0, loading a collection whose stored version is greater than the running application version panics with Collection version is greater than application version, so a rollback a minor back cannot read the collections the newer binary wrote.
One more open report belongs in the runbook. An issue filed in January 2026 and still open describes cluster snapshot restores from a three node, three shard, two replica setup that sometimes produce three replicas instead of two, inconsistently across shards and runs, leaving replicas in a Partial state. It is reported as intermittent with no stated root cause, so read cluster state after every restore rather than treating it as a defect tied to a version you can avoid.
milvus-backup has the same shape of trap in a different place
Milvus moves the version constraint out of the release cadence and into a published matrix, which is easier to plan against and just as binding.
The v0.5.16 release, published 29 May 2026, is the interface to write against. Its README lists eight subcommands: check, create, delete, get, help, list, restore and server, and one global flag beyond help, --config, defaulting to backup.yaml. The development branch carries more than the release does, so pin the runbook to a tag.
Storage is configured in two halves. minio.storageType names the backend Milvus itself uses and minio.backupStorageType the backend the backup is written to, both taking local, minio, s3, aws, gcp, ali(aliyun), azure, tc(tencent) or gcpnative. Set them differently and crossStorage, false by default, permits the copy. Check the shipped defaults: accessKeyID and secretAccessKey are both minioadmin, bucketName is a-bucket, rootPath is files, port is 9000 and useSSL is false.
The constraint to size windows around is that there is no incremental mode. The request for incremental backup and restore was filed on 11 August 2025 and remains open, so every backup is a full copy and every restore a full write. For a corpus that grows monotonically, the backup window grows with it.
# Check the resolved config and the connections first ./milvus-backup check ./milvus-backup create -n orders_2026_08_30 ./milvus-backup list # Restore into suffixed collections, and bring the index back with the data ./milvus-backup restore --restore_index -n orders_2026_08_30 -s _recover
--restore_index is not optional in practice: the end-to-end guide at that tag says that without it you restore the index manually, which is the Milvus version of a restore that reports success and leaves you something you cannot query at production latency. On the wider engine choice, we compared how these engines compare on everything other than recovery elsewhere.
| Engine | Rule as published | Worked example | Direction that is hard-blocked |
|---|---|---|---|
| Qdrant | A snapshot restores only to the same minor version or the next minor version | A v1.18.1 snapshot restores to v1.18.x where x is 1 or greater, or to v1.19.x | Restoring into an older build: loading a collection whose stored version exceeds the running version panics with Collection version is greater than application version |
| milvus-backup v0.5.16 | Backups supported from Milvus 2.2 and later, restores to Milvus 2.4 and later, and only to the same or a newer Milvus version | A 2.5 backup restores to 2.5 or 2.6, not to 2.4 | Any downgrade |
A restore drill that produces evidence, not a green tick
Start with the arithmetic. Your recovery time objective for a retrieval index is the elapsed time from incident to the collection serving queries at the recall you promised, and the comparison is restore against rebuilding the index from source documents. Measure both on your own hardware and corpus: time a restore of a real archive, then time a re-embed and re-index of the same data. We are not going to hand you throughput numbers, because we found no reproducible published figure for these engines at a stated corpus size and a made-up one sets your objective wrong. The cost side of the re-embed branch is what re-embedding the corpus from source actually costs.
One structural difference tilts that arithmetic. A Qdrant snapshot contains the collection configuration alongside the points, so a restored collection comes back with the HNSW and quantization settings it had. A PostgreSQL dump is not like that: pg_dump defines post-data items as including definitions of indexes, triggers, rules and constraints, so the dump carries the index definition and the index is built again at restore, which is why the pgvector index build is the part that takes hours.
Now the drill. Restore into a scratch collection, never over the live one, and verify with requests rather than with the restore's own status code.
# 1. Exact point count on the restored collection
curl -X POST 'http://localhost:6333/collections/orders_restore_drill/points/count' \
-H 'api-key: ********' \
-H 'Content-Type: application/json' \
-d '{"exact": true}'
# 2. Per-shard replica states: every replica should read Active
curl -X GET 'http://localhost:6333/collections/orders_restore_drill/cluster' \
-H 'api-key: ********'
# 3. Collection config, to confirm the index parameters came back with it
curl -X GET 'http://localhost:6333/collections/orders_restore_drill' \
-H 'api-key: ********'Step 2 is where the Partial replica state from that open cluster-restore issue would appear, and a point count will never make that check for you. A non-zero count is necessary and nowhere near sufficient: it proves the restore path ran.
Five checks make up the record worth keeping.
The last two turn this into an engineering control instead of a checkbox, and they need an artefact you may not have yet: a fixed set of queries with known-good results, which is the retrieval eval set you run against the restored collection. Without it, "the index came back" is an opinion.
Do this one this week. Take a collection snapshot on every node, restore it into a scratch collection with priority set explicitly in the right part of the request for the endpoint you chose, run the three calls above, and write the point count, the replica states and the date into the runbook beside the Qdrant version you ran them against. That last field expires: when your estate moves two minors on, today's archives stop being restorable. For the rest of our RAG systems work, including the retrieval quality side that decides what "recovered" even means, start at the pillar.
| Check | How | Passes when |
|---|---|---|
| The restore ran | POST /collections/{drill}/points/count with {"exact": true} | Count matches the source collection's count at snapshot time |
| No shard is stuck | GET /collections/{drill}/cluster | Every replica reads Active, none reads Partial |
| Index parameters survived | GET /collections/{drill} | HNSW and quantization config match the source collection |
| Retrieval still works | Run the retrieval eval set against the drill collection | Recall at k within the tolerance you set for production |
| Results are the same results | Diff top-k document ids against production for the eval queries | Differences are explained by writes after the snapshot instant, nothing else |
FAQ
Quick answers to the questions this post tends to raise.



