AnswersScale & Architecture

Why Do Upserts Slow HNSW Reads?

Why does a high volume of real-time upsert operations tank read performance in HNSW indexes?

2 min readJuly 2026

The short answer

Upserts slow reads because both operations fight over one data structure. Inserting into an HNSW graph rewires neighbor lists across multiple layers (the same lists queries are traversing at that moment), so writes take locks that reads feel.

Deletes are worse: the graph can't cheaply heal around a removed node, so engines tombstone it, and recall and latency decay until a rebuild or compaction that itself competes for the same CPU and memory. Under sustained upserts, p99 latency rises and recall drifts down at the same time.

What happens on an insert?

The new vector must find its neighbors and be linked into every layer it appears in. HNSW's insertion algorithm runs greedy searches through the graph plus mutations to existing nodes' neighbor lists. Each mutation is a synchronization point with concurrent queries. One insert is cheap; thousands per second are a standing tax on every traversal.

Why are deletes worse than inserts?

HNSW has no cheap delete. Removing a node properly means repairing every neighbor list that pointed at it, so engines mark it dead instead and filter tombstones at query time. Traversals now visit nodes that yield nothing, recall degrades as live paths thin out, and the eventual cleanup (rebuild or compaction) is a heavyweight job running alongside your traffic. The recall-degradation-under-updates problem for graph indexes is documented in FreshDiskANN (Singh et al., 2021), which exists precisely because naive streaming updates break graph ANN quality.

How do systems cope?

The common patterns all reduce contention on the serving structure: segment the index and merge in the background (LSM-style), route writes to a separate ingest path that builds fresh segments off the serving path, or decouple indexing from serving entirely so queries never share locks with ingestion. Look for one property: reads served from immutable structures, writes absorbed elsewhere, and merges running asynchronously.

This is how TopK's write path actually works: a log-writer service appends writes to a WAL backed by object storage, a separate compactor service consumes the log and builds read-optimized indexed files (LSM-style, with compaction planned and executed in parallel), and query executors only ever read immutable segments. Read and write paths run on separate node pools, so ingestion never takes a lock a query can feel.

TopK's decoupled write path: queries read immutable segments, so writes never contend with reads.

Writes

upserts, deletes

WAL

durable on object storage

Compactor

builds indexed segments

Immutable segments

Query executors

reads never blocked

TopK's stated motivation is the failure mode this page describes: in coupled architectures, "write traffic can negatively affect queries, which usually leads to over-provisioning of resources to maintain SLAs".

What should you do?

If your workload has real write volume, benchmark under mixed read/write load, never read-only (how to benchmark), and prefer engines whose ingest path is decoupled from the query path.

The measured effect of this design in TopK: dynamic write batching sustains about 70MB/s of ingest, over 30,000 vectors per second, without sharing resources with the read path; concurrent-load measurements are on the benchmarks page.