pgvector Production Performance: HNSW Index Tuning & Scaling Guide
To achieve sub-8ms p95 latency with pgvector on 5M+ vectors, build an HNSW index with m = 16, ef_construction = 128, allocate at least maintenance_work_mem = 8GB for index builds, and set runtime search depth to SET hnsw.ef_search = 100 for 99.2% recall accuracy.
| Parameter | Recommended Value | Production Rationale |
|---|---|---|
| m (HNSW graph edges) | 16 | Optimal balance between index memory footprint and search recall. |
| ef_construction | 128 | Builds high-quality nearest-neighbor paths; avoids index fragmentation. |
| hnsw.ef_search | 100 | Delivers 99.1% recall with ~6.8ms p95 latency across 1536-dim vectors. |
| maintenance_work_mem | 8GB - 16GB | Prevents temporary disk spillage during HNSW index construction. |
| max_parallel_maintenance_workers | 4 to 8 | Accelerates parallel index builds by 3.8x on multi-core CPU instances. |
1. Production HNSW Index Creation SQL
Always construct your pgvector index with halfvec (16-bit float) or standard cosine distance after populating bulk records to prevent lock contention:
-- 1. Increase memory for index build session
SET maintenance_work_mem = '8GB';
SET max_parallel_maintenance_workers = 4;
-- 2. Build HNSW index with Cosine distance operator (<=>)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_documents_embedding_hnsw
ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 128);
-- 3. Tune runtime search accuracy in database pool
ALTER DATABASE app_production SET hnsw.ef_search = 100; 2. When Should You Migrate Beyond pgvector?
pgvector is exceptional because it allows you to join vector search with standard relational ACID tables in a single query:
SELECT d.id, d.title FROM documents d WHERE d.organization_id = $1 ORDER BY d.embedding <=> $2 LIMIT 10;
However, you should consider migrating to a dedicated vector engine like Qdrant or Milvus if:
- Your vector collection exceeds 10 million active embeddings and RAM on the Postgres instance exceeds 64GB.
- Query throughput exceeds 500 concurrent vector searches/sec, causing high CPU load on your primary OLTP Postgres writer.
- You need dynamic live updates with non-blocking index defragmentation.