Qdrant’s async upsert with wait=False returns immediately without confirming the write succeeded. When your vectors do not match the collection’s expected dimension, the write silently fails and no exception fires. The count then stays at zero. Here is the correct pattern to verify writes without losing async speed.
Why wait=False can silently fail
With wait=True (default), Qdrant confirms every point was written before returning. Vector-size mismatch, invalid IDs, or storage errors raise exceptions synchronously.
With wait=False, the client sends the write to Qdrant’s queue and returns. If the write fails downstream (dimension mismatch, storage full, invalid vector), you have no way to know without checking the operation status or count afterward.
Fix 1: use wait=True for correctness-critical paths
from qdrant_client import QdrantClient
from qdrant_client.models import PointStruct
client = QdrantClient(host="localhost", port=6333)
# CORRECT for production writes: wait=True raises on error
result = client.upsert(
collection_name="docs",
wait=True,
points=[
PointStruct(id=1, vector=[0.1] * 1536, payload={"text": "doc 1"}),
PointStruct(id=2, vector=[0.2] * 1536, payload={"text": "doc 2"}),
],
)
print(f"Upsert status: {result.status}") # "completed"Fix 2: verify count after batch upsert
def upsert_verified(client, collection_name, points, batch_size=100):
"""Upsert with count-based verification. Detects silent failures."""
before = client.count(collection_name=collection_name).count
for i in range(0, len(points), batch_size):
batch = points[i:i + batch_size]
client.upsert(
collection_name=collection_name,
wait=True,
points=batch,
)
after = client.count(collection_name=collection_name).count
added = after - before
expected = len(points)
if added < expected:
raise RuntimeError(
f"Silent failure: expected {expected} new points, got {added}. "
f"Check vector size vs collection config."
)
return added
added = upsert_verified(client, "docs", points)
print(f"Successfully added {added} points")Fix 3: async upsert with status polling
import time
from qdrant_client.models import UpdateStatus
def upsert_async_verified(client, collection_name, points):
"""Fire-and-forget upsert with follow-up status check."""
op = client.upsert(
collection_name=collection_name,
wait=False,
points=points,
)
op_id = op.operation_id
# Poll status (fast for successful writes)
for _ in range(30):
status = client.get_operation_status(collection_name, op_id)
if status == UpdateStatus.COMPLETED:
return True
if status in (UpdateStatus.FAILED, UpdateStatus.CANCELLED):
raise RuntimeError(f"Upsert failed: {status}")
time.sleep(0.1)
raise TimeoutError("Upsert did not complete in 3 seconds")Fix 4: pre-flight vector size check
def upsert_safe(client, collection_name, points):
"""Pre-check vector dimensions match collection config."""
info = client.get_collection(collection_name)
expected_size = info.config.params.vectors.size
for i, point in enumerate(points):
actual_size = len(point.vector)
if actual_size != expected_size:
raise ValueError(
f"Point {i} vector size {actual_size} does not match "
f"collection dimension {expected_size}"
)
return client.upsert(
collection_name=collection_name,
wait=True,
points=points,
)Debugging checklist
- After every batch upsert, log the collection count. If it does not increase by the expected amount, investigate.
- Check collection dimension:
client.get_collection(name).config.params.vectors.size. Compare to your embedding output length. - For production ingest, use
wait=Trueand handle exceptions. Only usewait=Falsefor fire-and-forget analytics. - If migrating between embedding models, ALWAYS recreate the collection. Different models produce different dimensions.
Common mistakes with Qdrant upsert
- Using
wait=Falseto make batch ingest faster without verification. Speed gain is not worth the silent-failure risk. - Ignoring the returned status object. Even
wait=Truereturns a status. Check it. - Assuming your embedder output matches the collection. Embedding models can change output dimension between versions. Always verify.
- Not resetting collection when switching embedding model. Mixed embeddings in one collection produce useless similarity scores.
Official documentation
Common causes of Qdrant silent upsert failures
When you set wait=False on a Qdrant upsert, the client returns immediately without waiting for the operation to complete. This is faster but comes with a real cost: you have no idea if the upsert actually succeeded. Here are the four most common causes of silent failures I see in production RAG pipelines in 2026.
- Vector dimension mismatch. Your collection was created with 768-dim vectors but you send 512-dim vectors from a different embedding model. Qdrant rejects the upsert but with
wait=False, your code never sees the error. - Payload size exceeds limits. Default max payload size is 10 MB per point. Documents with large text fields or metadata blobs get silently rejected. Your batch appears to succeed but nothing lands.
- Collection does not exist. If your collection was deleted or never created, upserts silently fail with
wait=False. Always checkclient.collection_exists()before starting a batch job. - Authentication or connection timeout. If your Qdrant Cloud credentials expired or the network dropped mid-batch, subsequent upserts fail silently while your Python code marches on happily.
How to add operation verification to Qdrant upserts
The safest pattern in 2026 is to use wait=True for critical writes and add a post-write count check for batch operations. Here is the pattern I use in production RAG pipelines.
from qdrant_client import QdrantClient
from qdrant_client.models import PointStruct
client = QdrantClient(url="http://localhost:6333")
def safe_upsert(collection, points, batch_size=100):
total_written = 0
for i in range(0, len(points), batch_size):
batch = points[i:i + batch_size]
result = client.upsert(
collection_name=collection,
points=batch,
wait=True, # blocking, safer for correctness
)
if result.status != "completed":
raise RuntimeError("Upsert batch " + str(i) + " failed: " + str(result.status))
total_written += len(batch)
# Post-write count check catches any silent drops
count = client.count(collection_name=collection).count
print("Total points in collection: " + str(count))
return total_writtenFor production RAG systems in 2026, treat wait=False as an optimization only for non-critical background operations. Any upsert whose failure would break user-facing search should always use wait=True. The performance cost is minimal for batch sizes under 500 points, and the correctness guarantee saves hours of debugging phantom data loss issues.
One final pattern: for critical writes in production RAG systems, log every successful upsert and the total count immediately after each batch. This gives you an audit trail if data goes missing weeks later. Add a monitoring alert if the collection count fails to grow after a scheduled write. Silent failures caught 2 weeks after the fact are much more expensive than the small overhead of proper verification. This is the single highest-leverage habit you can build for reliable Qdrant integration in 2026 production systems.
Frequently Asked Questions
When is wait=False actually the right choice?
For high-throughput streaming ingest where you accept eventual consistency. Analytics event streams, audit logs, and telemetry pipelines fit. RAG document ingest does not.
Does wait=True slow down ingest much?
Slightly. wait=True waits for one Qdrant round trip after write. Batch upsert amortizes this over hundreds of vectors. For most workloads the overhead is under 10%.
Can I mix wait=True and wait=False?
Yes. Use wait=True for ingest paths where correctness matters. wait=False for background enrichment writes where a lost update is acceptable.
Does get_operation_status add overhead?
Minor. One extra HTTP call per polling cycle. Poll interval of 100ms is common. Total overhead per batch is 100-300ms which is often faster than pure wait=True on the specific batch.
Are Qdrant operations idempotent?
Upsert by ID is idempotent (updates in place). Insert with duplicate ID overwrites the existing point. Safe to retry the same upsert.
