Qdrant’s default snapshot path is ./snapshots, relative to the storage directory. That one config default means every snapshot you create through the API lands on the same filesystem, same volume, same physical SSD as the live collection data. You run POST /collections/memory/snapshots, you get a 200 back, you feel responsible and safe. You are neither. When that disk fails, the snapshot dies in the same instant as the data it was supposed to protect.

What I expected

A snapshot API implies a safety mechanism. The mental model most of us carry is: live data is fragile, snapshots are the recovery path, so by definition they must live somewhere safer. Proxmox reinforces this instinct because VM snapshots and backups feel like separate things, and Proxmox Backup Server actually does ship your VM backups to a different box.

Vector databases don’t work that way out of the box. Qdrant, Chroma, and Milvus all treat snapshots as a serialization feature, not a disaster recovery feature. The snapshot endpoint answers the question “give me a portable copy of this collection” and writes the answer to local disk. Where that local disk lives is your problem.

The expectation gap is worse in a homelab because the deployment is usually one container on one node with one bind mount:

# The setup that feels fine until it isn't
services:
  qdrant:
    image: qdrant/qdrant:v1.13.4
    volumes:
      - /mnt/nvme0/qdrant:/qdrant/storage
    ports:
      - "6333:6333"

Everything under /qdrant/storage sits on nvme0. Collections, WAL, and the snapshots/ directory the API writes into. One mount point, one blast radius.

What actually happens

Two separate failures hide in this setup, and the second one is sneakier than the first.

Failure one: physical co-location

Consumer NVMe drives in homelabs run hot, run constantly, and accumulate writes fast when a vector DB is doing HNSW index maintenance and WAL flushes underneath an agent that stores memories on every session. When the drive goes (wear-out, a flaky controller, or the classic SATA cable that worked fine for two years), you lose the collection and every snapshot of it simultaneously.

The trap is that snapshots make you stop worrying. You’ve automated a nightly snapshot job, the API returns success, ls shows a growing list of .snapshot files. Every observable signal says “protected.” The only signal that says otherwise is df output showing that the snapshots share a device with the data, and nothing ever prompts you to look at that. It’s the same gap I wrote about with Longhorn volume health: the system reports healthy because it’s checking the wrong property. A snapshot that exists is not the same as a snapshot that survives the failure you’re snapshotting against.

Failure two: logical incompatibility

Suppose you dodge the physical problem. You copy snapshots to your NAS, the disk fails, you rebuild the node, and you restore. The restore succeeds. Then your MCP server starts throwing errors on every search:

UnexpectedResponse: Status code 400 (Bad Request)
"Wrong input: Not existing vector name error: fast-nomic-embed-text-v1.5-q"

This is the second trap: the snapshot faithfully preserved a schema your tooling no longer speaks. mcp-server-qdrant moved from unnamed (default) vectors to named vectors around v0.6.0. Collections created by older versions store points with a bare vector; newer versions query by a specific vector name derived from the embedding model, like fast-nomic-embed-text-v1.5-q. If you run the server with uvx, which pulls the latest release on invocation, the client upgrades itself silently while your data stays frozen at the old schema. The restored collection is byte-perfect and functionally useless.

The difference in the collection config looks like this:

// Old collection: unnamed default vector
{ "params": { "vectors": { "size": 768, "distance": "Cosine" } } }

// What the current client expects: named vector
{ "params": { "vectors": {
    "fast-nomic-embed-text-v1.5-q": { "size": 768, "distance": "Cosine" }
} } }

Qdrant has no in-place rename for this. You can’t patch a collection from unnamed to named vectors. The only path is a migration: read everything out, write everything into a new collection with the right schema.

So the full failure mode is a chain. Snapshots co-located with data cover you against nothing physical. Snapshots moved off-disk cover you against hardware but not against schema drift. Real recovery needs both physical separation and a tested restore path against the current client version.

The fix

Three layers, in order of how much pain they remove.

1. Put snapshots on a different device

Qdrant exposes the snapshot path as config. Point it at a mount backed by a different physical disk, or better, network storage:

services:
  qdrant:
    image: qdrant/qdrant:v1.13.4
    environment:
      - QDRANT__STORAGE__SNAPSHOTS_PATH=/qdrant/snapshots
    volumes:
      - /mnt/nvme0/qdrant:/qdrant/storage        # live data: fast local NVMe
      - /mnt/nas/qdrant-snapshots:/qdrant/snapshots  # snapshots: NFS to the NAS

One caveat: Qdrant builds snapshots with hard links where possible and needs a writable temp area, so snapshot creation over NFS is slower and briefly doubles disk usage during packaging. For collections in the single-digit-gigabyte range typical of agent memory, this is a non-issue. If it becomes one, snapshot to a second local disk and rsync from there.

If you’re on Kubernetes with Longhorn instead of a single container, the equivalent move is a separate PVC on a different storage class (or at minimum different replica placement) for the snapshot path. Same principle: the recovery artifact must not share a failure domain with the thing it recovers.

2. Ship snapshots off the node entirely

A different disk in the same machine still shares a power supply, a mainboard, and your tendency to do risky things to that machine at 11pm. Get snapshots off the node:

#!/usr/bin/env bash
# nightly-qdrant-snapshot.sh
set -euo pipefail

COLLECTION="memory"
QDRANT="http://10.0.0.50:6333"

# Trigger snapshot, capture the generated name
SNAP=$(curl -sf -X POST "$QDRANT/collections/$COLLECTION/snapshots" \
  | jq -r '.result.name')

# Pull it over HTTP and push to object storage
curl -sf "$QDRANT/collections/$COLLECTION/snapshots/$SNAP" \
  -o "/tmp/$SNAP"
rclone copy "/tmp/$SNAP" "minio:qdrant-snapshots/$COLLECTION/"
rm "/tmp/$SNAP"

# Keep the node's snapshot dir from filling up
curl -sf -X DELETE "$QDRANT/collections/$COLLECTION/snapshots/$SNAP"

The detail worth noticing: Qdrant lets you download snapshots over the REST API, so the copy job doesn’t need filesystem access to the Qdrant host at all. It can run anywhere that can reach port 6333, which makes it trivial to run as a Kubernetes CronJob or a container on a different node. Restore works the same way in reverse, with PUT /collections/{name}/snapshots/recover accepting a URL.

3. Handle the schema, not just the bytes

For the named-vector migration, snapshots don’t help. You need to move the data through the API so it gets re-indexed under the new vector name. The scroll endpoint does the export side:

from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct

client = QdrantClient(url="http://10.0.0.50:6333")
VECTOR_NAME = "fast-nomic-embed-text-v1.5-q"

client.create_collection(
    collection_name="memory-v2",
    vectors_config={VECTOR_NAME: VectorParams(size=768, distance=Distance.COSINE)},
)

offset = None
while True:
    points, offset = client.scroll(
        collection_name="memory",
        limit=100,
        with_payload=True,
        with_vectors=True,   # omitting this silently drops the vectors
        offset=offset,
    )
    client.upsert(
        collection_name="memory-v2",
        points=[
            PointStruct(id=p.id, payload=p.payload, vector={VECTOR_NAME: p.vector})
            for p in points
        ],
    )
    if offset is None:
        break

This works when the embedding model hasn’t changed, only the vector naming. If the model changed too (different dimensions, different quantization), the vectors themselves are stale and you have to re-embed from the payloads instead of copying vectors across. That’s the strongest argument for always storing the source text in the payload: payloads are the durable asset, vectors are a derived cache you can rebuild.

Afterward, verify the schema instead of assuming:

curl -s http://10.0.0.50:6333/collections/memory-v2 \
  | jq '.result.config.params.vectors | keys'
# ["fast-nomic-embed-text-v1.5-q"]  <- named vector present, client will be happy

And pin your MCP server version so the client can’t drift out from under the data again. uvx mcp-server-qdrant pulls latest; uvx mcp-server-qdrant==0.7.1 pulls what you tested against. Upgrade deliberately, with a migration plan, not as a side effect of a session starting.

Why this matters

For a lot of homelab services, losing a disk means re-running an Ansible playbook and moving on. Vector databases are different because the data is accumulated, not declared. If your agents write session learnings, decisions, and episodic memory into Qdrant (the pattern behind my Karpathy-style LLM wiki and the recall layer in vector search vs activation-based memory), then months of that accumulation lives in one collection. There’s no Git repo to re-clone. The GitOps reflex of “the cluster is disposable, the repo is truth” fails exactly here, because the vector DB is the truth for that data. Anyone building agent memory systems for real workloads (something I help teams with) hits this eventually: the memory layer quietly becomes the most irreplaceable state in the whole stack, while getting the least backup attention.

The general lessons:

Check where your snapshots physically land, today. Not where you think they land. df -h on the snapshot directory, compare the device against the data directory. This takes thirty seconds and is the whole first half of this post.

A backup you haven’t restored is a hypothesis. The 400 error above only surfaces when you actually recover a snapshot and run the current client against it. A quarterly restore drill into a scratch collection would catch schema drift months before it matters. Nobody does quarterly restore drills for their homelab vector DB, which is why pinning client versions is the practical substitute.

Snapshots and backups answer different questions. A snapshot answers “give me a copy of this collection’s current state.” A backup answers “can I get my data back after the worst realistic failure.” The first is a feature the database gives you. The second is a property of your whole setup, and no single API call provides it.

Payloads outlive vectors. Embedding models change, quantization schemes change, vector naming conventions change. The source text in your payloads survives all of it. Design your collections so that a full re-embed from payloads is always possible, and half of these migration problems downgrade from “data loss” to “an afternoon of compute.”

The fix costs one config line and a cron job. The failure costs every memory your agents ever stored. That’s about as lopsided as risk math gets.