Symptoms
GET /collections/kb_chunksshows"status": "yellow"for a long time, or"grey".indexed_vectors_countis much lower thanpoints_count.- Searches are slow and CPU is high right after a bulk load.
- Upload throughput drops as the collection grows.
What the statuses mean
| Status | Meaning |
|---|---|
green | All good; no optimisation running |
yellow | Optimisation running (indexing, merging or vacuuming segments). Searches work |
grey | Optimisations needed but not triggered, typically after a restart. Nothing will happen until an update arrives |
red | An optimisation failed; check logs |
Searches work in all states except some red cases. They may be slower while segments are unindexed, because unindexed segments are searched by brute force.
Step 1: read the collection info
curl -s -H "api-key: $QDRANT_API_KEY" http://qdrant:6333/collections/kb_chunks | \
jq '.result | {status, optimizer_status, points_count, indexed_vectors_count, segments_count, config: .config.optimizer_config}'
| Field | Look for |
|---|---|
optimizer_status | ok, or an error message |
indexed_vectors_count vs points_count | Gap = vectors in segments not yet HNSW-indexed |
segments_count | Very high numbers slow everything down |
indexing_threshold | Segment size (KB of vectors) above which a segment gets an HNSW index |
indexed_vectors_count won't always equal points_count: small segments under indexing_threshold are intentionally left unindexed because brute force on them is fast. A large, persistent gap on a big collection is the problem.
Step 2: grey status - trigger the optimizer
Grey appears when Qdrant restarts with optimisations pending. Trigger them with an update to the collection; an empty optimizer config update is enough:
curl -X PATCH http://qdrant:6333/collections/kb_chunks \
-H "api-key: $QDRANT_API_KEY" -H "Content-Type: application/json" \
-d '{"optimizers_config": {}}'
Any point upsert also triggers it. Status should move to yellow, then green.
Step 3: yellow for hours - is it progressing?
# repeat every few minutes and compare
curl -s -H "api-key: $QDRANT_API_KEY" http://qdrant:6333/collections/kb_chunks | jq '.result.indexed_vectors_count'
| Progress | Cause | Fix |
|---|---|---|
| Rising steadily | Normal indexing of a large upload | Wait; consider more CPU for the next load |
| Rising then resetting | Continuous writes keep creating new segments to optimise | Batch writes; tune below |
| Not moving | Optimizer starved of CPU, or error | Check logs and max_optimization_threads; look for disk full |
kubectl logs -n qdrant qdrant-0 | grep -iE "optimi|error|panic|disk" | tail -30
Optimisation needs temporary disk space (it writes new segments before dropping old ones). A nearly full disk stalls it.
Step 4: bulk uploads the fast way
Uploading millions of points into a collection with HNSW enabled makes Qdrant build indexes on segments that are later merged and indexed again. For an initial load, defer the graph build:
from qdrant_client import QdrantClient, models
client = QdrantClient(url="http://qdrant:6333", api_key=API_KEY, timeout=120)
# 1. disable HNSW graph building during the load
client.update_collection("kb_chunks", hnsw_config=models.HnswConfigDiff(m=0))
# 2. upload in parallel batches
client.upload_points(
"kb_chunks",
points=points_iter, # generator of models.PointStruct
batch_size=256,
parallel=4,
max_retries=3,
)
# 3. re-enable HNSW; the optimizer builds the graph once
client.update_collection("kb_chunks", hnsw_config=models.HnswConfigDiff(m=16))
Older guides set indexing_threshold: 0 to disable indexing during uploads; current Qdrant documentation recommends m: 0 for this. Use whichever your version's docs describe, and always switch it back.
Other bulk-load tips:
| Tip | Why |
|---|---|
| Create payload indexes before uploading | Filter-aware graph links built in the same pass |
Use gRPC (prefer_grpc=True) | Lower serialisation overhead |
wait=False on upserts during bulk load | Don't block on each batch's application |
Fewer, larger segments: default_segment_number matching CPU cores | Less merging later |
Vectors on_disk: true if RAM is tight | Keeps upload from exhausting memory |
Step 5: steady-state tuning
| Setting | Effect |
|---|---|
optimizer_config.indexing_threshold | Larger = fewer small indexes built, more brute-force on small segments |
optimizer_config.max_optimization_threads | CPU given to optimisation; limit it if search latency suffers during optimisation |
optimizer_config.deleted_threshold | Fraction of deleted points that triggers vacuum of a segment |
optimizer_config.default_segment_number | Target segment count; usually ≈ CPU cores |
Change with PATCH /collections/{name} and optimizers_config.
Step 6: segment count too high
Thousands of segments usually come from very frequent small writes with wait=true, or settings that stop merging. Check max_segment_size and that the merge optimizer isn't throttled to zero threads. Let the optimizer catch up off-peak.
Decision table
| Status / finding | Action |
|---|---|
| Grey | PATCH empty optimizers_config |
| Yellow, progressing | Wait; plan bulk loads with m: 0 |
| Yellow, stuck | Logs, disk space, optimizer threads |
| Red | Logs; fix cause (often disk); trigger again |
| Big index gap after load | Bulk-load procedure next time |
Prevent it next time
- Script bulk loads: create collection + payload indexes →
m: 0→ upload →m: 16→ wait for green. - Alert on collections not green for more than an hour, and on grey after restarts.
- Keep 30%+ free disk for optimisation.
- Record load time per million points to plan the next one.