RAGFlow Vector Search Latency Spikes to 4 Seconds: A Practical Fix for HNSW Locks and Normalization

Fix pgvector latency and recall issues under high concurrency with read-write index separation, L2 normalization, and RRF hybrid retrieval.
The article documents a vector retrieval incident when integrating RAGFlow into an Agent memory pipeline: 50 QPS writes caused query latency to spike from 80ms to 4 seconds and recall to drop 30%. Root cause one was pgvector's HNSW index write-lock contention and convoy effect; the solution was read-write index separation—IVFFlat for writes, HNSW for reads, with async HNSW rebuild—boosting insert throughput from 50/s to 180/s and reducing p95 latency to 120ms. Root cause two was unnormalized embeddings; after L2 normalization, recall@10 improved from 0.68 to 0.91. A hybrid RRF approach further improved answer relevance by 23%. The article emphasizes that index strategy and embedding hygiene matter more than model selection.
When integrating RAGFlow into an Agent memory pipeline, we encountered a classic vector search production incident: as document insertion concurrency reached around 50 QPS, query latency spiked from 80ms to over 4 seconds, while recall simultaneously dropped by 30%. The culprits were two often-overlooked low-level details—pgvector's HNSW index write-lock contention, and unnormalized embedding vectors.

RAGFlow itself is a solid open-source RAG engine that blends retrieval and Agent capabilities well, but like all pgvector-based vector systems, it exposes PostgreSQL's inherent temperament under high load. Below we break down the specific failure points and the fix solutions validated in production.
Root Cause 1: HNSW Index Write-Lock Contention
pgvector's HNSW index is not designed for high-concurrency writes. When bulk-inserting documents, we observed INSERT queries blocking each other, forming a classic "convoy effect": one write transaction holds the lock, subsequent writes queue up waiting, and query threads get dragged into the wait queue too, causing the latency curve to steepen instantly.
Fix: Separate Read and Write Indexes
The core idea is to route writes through IVFFlat and reads through HNSW, with each index playing its role. Create both vector indexes when building the table:
CREATE TABLE agent_memory_chunks (
id BIGSERIAL PRIMARY KEY,
agent_id VARCHAR(64) NOT NULL,
chunk_text TEXT NOT NULL,
embedding vector(1536) NOT NULL,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT NOW()
);
-- Write-friendly: IVFFlat, low lists count
CREATE INDEX idx_memory_ivfflat ON agent_memory_chunks
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- Read-friendly: HNSW, tuned parameters
CREATE INDEX idx_memory_hnsw ON agent_memory_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
IVFFlat does not produce global blocking on insert, while HNSW focuses on serving read queries. The HNSW index is asynchronously rebuilt during off-peak hours using REINDEX INDEX CONCURRENTLY to avoid table locking. Queries include an agent_id filter to narrow the scan range using a composite index:
SELECT id, chunk_text, metadata,
1 - (embedding <=> $1::vector) AS similarity
FROM agent_memory_chunks
WHERE agent_id = $2
AND metadata @> $3::jsonb
ORDER BY embedding <=> $1::vector
LIMIT 20;
This change directly resolved the write-write blocking problem, boosting insert throughput from 50/s to 180/s. Interestingly, index separation is not a silver bullet—it increases storage overhead and maintenance complexity, and suits Agent memory scenarios where write and read patterns differ significantly; applications with mostly reads and few writes may not need it.
HNSW (Hierarchical Navigable Small World) is an approximate nearest neighbor search algorithm based on a multi-layer graph structure. Each layer's graph consists of nodes connected at different densities; queries perform greedy search from the sparse top layer downward, ultimately locating candidate neighbors in the dense bottom layer. Its advantages are high recall and low query latency, but the drawback is the need to maintain the graph structure: each insert updates the connection lists of multiple neighbor nodes and may trigger graph reorganization. This dynamic maintenance leads to write amplification and lock contention, especially under high-concurrency writes where multiple transactions contend for latches on the same index page, forming convoy blocking. In contrast, IVFFlat partitions the vector space into multiple Voronoi cells via clustering; inserts only need to compute the assigned cluster and append to the corresponding list, without global graph reconstruction, thus offering higher write throughput, but queries must scan multiple clusters, and recall is typically lower than HNSW.
In PostgreSQL, REINDEX INDEX CONCURRENTLY is an online index rebuild method that allows read and write operations on the table without blocking during the rebuild (though it consumes more CPU and I/O). The principle is to create a new index copy, synchronize table changes to the new index via a temporary log, and finally swap old and new indexes within a brief lock. This is safer than a regular REINDEX, which holds an exclusive lock and renders the table unavailable. Note that CONCURRENTLY mode cannot be used for expression indexes or certain constraint indexes, and if it fails midway, it leaves an invalid index that must be manually cleaned up. When rebuilding an HNSW index, it's advisable to choose off-peak hours and monitor the pg_stat_progress_create_index view to assess progress.
The lists parameter of an IVFFlat index determines the number of cluster centers and directly affects query performance and recall: larger lists means smaller clusters, potentially fewer clusters to scan during queries (depending on the probes parameter), but longer index build time; smaller lists means larger clusters, requiring more data to be scanned during queries, but faster writes. It's generally recommended to set lists to the square root of the row count (e.g., 1000 for 1 million rows), and set ivfflat.probes to around sqrt(lists) during queries. However, in write-intensive scenarios, a smaller lists (e.g., 100) can significantly reduce clustering update overhead on inserts. Therefore, with read-write separation, the write index can use a lower lists to maximize write throughput, while the read index uses HNSW to ensure query quality.
Root Cause 2: Unnormalized Embeddings Drag Down Recall
The second problem is more subtle. We initially stored the raw embedding vectors returned by the OpenAI API without normalization. pgvector's cosine similarity calculation mathematically expects unit vectors, but raw embeddings have varying magnitudes, causing semantically similar chunks to be ranked lower.
The fix is simple: perform L2 normalization before writing.
import numpy as np
from typing import List
def normalize_embedding(embedding: List[float]) -> List[float]:
"""L2 normalization for cosine similarity."""
vec = np.array(embedding)
norm = np.linalg.norm(vec)
if norm == 0:
return embedding
return (vec / norm).tolist()
After normalization, recall@10 on the test set jumped from 0.68 to 0.91. This change costs almost nothing but yields more benefit than most model tuning attempts.
Hybrid Retrieval: Fusing Vector and Full-Text Search with RRF
The shortcoming of pure vector retrieval is exact keyword matching. When users search for specific terms, error codes, or names in Agent memory, vector similarity may not hit them. We combined pgvector with PostgreSQL full-text search, merging the two result sets using Reciprocal Rank Fusion (RRF).
import psycopg2
from typing import List, Tuple
def hybrid_search_rrf(conn, query_embedding, query_text, agent_id, k=20, rrf_k=60):
cursor = conn.cursor()
# Vector search ranking
cursor.execute("""
SELECT id, chunk_text, ROW_NUMBER() OVER (ORDER BY embedding <=> %s::vector) as rank
FROM agent_memory_chunks WHERE agent_id = %s
ORDER BY embedding <=> %s::vector LIMIT %s
""", (query_embedding, agent_id, query_embedding, k))
vector_ranks = {row[0]: (row[1], row[2]) for row in cursor.fetchall()}
# Full-text search ranking
cursor.execute("""
SELECT id, chunk_text, ROW_NUMBER() OVER (
ORDER BY ts_rank(to_tsvector('english', chunk_text),
plainto_tsquery('english', %s)) DESC) as rank
FROM agent_memory_chunks
WHERE agent_id = %s AND to_tsvector('english', chunk_text) @@ plainto_tsquery('english', %s)
LIMIT %s
""", (query_text, agent_id, query_text, k))
fts_ranks = {row[0]: (row[1], row[2]) for row in cursor.fetchall()}
# RRF fusion
fused_scores = {}
for doc_id in set(vector_ranks) | set(fts_ranks):
v_rank = vector_ranks.get(doc_id, (None, float('inf')))[1]
f_rank = fts_ranks.get(doc_id, (None, float('inf')))[1]
score = 1.0 / (rrf_k + v_rank) + 1.0 / (rrf_k + f_rank)
text = vector_ranks.get(doc_id, fts_ranks.get(doc_id))[0]
fused_scores[doc_id] = (text, score)
results = sorted(
[(did, t, s) for did, (t, s) in fused_scores.items()],
key=lambda x: x[2], reverse=True
)
return results[:k]
The RRF constant is set to 60, a common default in academic papers, which yields stable gains without tuning. In our A/B tests, hybrid retrieval improved answer relevance by 23%.
RRF (Reciprocal Rank Fusion) is an unsupervised rank fusion algorithm whose core formula is score(d) = Σ 1/(k + rank_i(d)), where k is a smoothing constant (typically 60) and rank_i(d) is the rank (starting from 1) of document d in the i-th retrieval result. The algorithm does not require normalizing scores from each retrieval path; it relies only on ranks, so it can effectively fuse retrievers with different scales (such as vector similarity and full-text search ts_rank). The constant k reduces the weight gap between highly ranked documents, preventing extreme rankings from one path from dominating the fused result. RRF has been widely validated in academia and industry as a simple and robust fusion method, especially suitable when the quality of two retrieval result sets is similar.
Production Metrics After the Fix
After deploying the three changes, key metrics recovered across the board:
- Insert throughput: 50/s → 180/s, a 3.6x improvement
- Query p95 latency: 4.2s → 120ms, roughly a 35x reduction
- recall@10: 0.68 → 0.91
- Hybrid retrieval brought a 23% improvement in answer relevance
This data illustrates an underappreciated fact: in vector retrieval systems, index strategy and embedding hygiene often matter more than model selection. The gains from switching to a larger embedding model may be less than a single normalization plus index separation.
Implications for RAG Engineering Practice
If you are evaluating RAGFlow or any vector-database-based RAG system, we recommend stress-testing the memory layer early in development rather than waiting until production traffic reveals bottlenecks. Specifically, write concurrency, recall distribution, and p95 latency should be incorporated into regular monitoring in CI or staging environments.
Another easily overlooked point is the asymmetry of read and write patterns. Agent memory scenarios are typically write-intensive and batched, with dispersed and real-time reads—this asymmetry is exactly what makes index separation effective. If the application is read-heavy and write-light, the cost of maintaining dual indexes may not be worthwhile; decide based on the actual load profile.
Finally, the original author emphasizes that this article contains no ads or sponsorships—it's purely a production debugging record. This transparent attitude is itself worth emulating—most vector retrieval pitfalls hide in the details, and real-world lessons shared by the community can save you more than official documentation.
One-line Summary
Solving pgvector latency and recall issues under high concurrency through read-write index separation, L2 normalization, and RRF hybrid retrieval.
Paragraph Summary
The article documents a vector retrieval incident when integrating RAGFlow into an Agent memory pipeline: 50 QPS writes caused query latency to spike from 80ms to 4 seconds and recall to drop 30%. Root cause one was pgvector's HNSW index write-lock contention and convoy effect under high-concurrency writes; the solution was read-write index separation—IVFFlat for writes, HNSW for reads, with asynchronous HNSW index rebuild—boosting insert throughput from 50/s to 180/s and reducing p95 latency to 120ms. Root cause two was unnormalized embedding vectors causing distorted cosine similarity ranking; after normalization, recall@10 improved from 0.68 to 0.91. Additionally, the article introduces a hybrid approach using RRF to fuse vector retrieval with full-text search, improving answer relevance by 23%. Ultimately, it emphasizes that index strategy and embedding hygiene matter more than model selection, and recommends early stress testing and monitoring of key metrics.
Related articles

LangChain Retry Middleware Configuration: Practices and Selection Guide
Based on Reddit community discussion, this article analyzes LangChain ModelRetryMiddleware retry parameter configuration practices, covering the selection logic and alternatives for key options like max_retries, backoff_factor, and jitter.

Texas Senate Race in Trouble: Why Are Republicans Nervous?
Texas Senate race stirs Republican anxiety. Based on street interviews, this article analyzes the Democratic offensive in funding, organization, and running everywhere, and how Republicans are defending "Texas, Texas" amid the political pendulum effect.

Gemini Chat Now Generates and Edits Google Docs Directly, a New Breakthrough in Office Productivity
Google Gemini's chat interface now generates and edits PDF, Google Docs, Sheets, Slides, Markdown, TXT, and code files. Available to all users; web supports all formats, mobile only PDF for now.