skip to content

How do deletes degrade an HNSW index over time, and what fixes it?

level: seniorimportance: should knowfreq 46%

answer

  1. deleted, but still walked through
  2. edges are load-bearing, so removal is unsafe
  3. the beam fills with unreturnable nodes
  4. memory does not come back until compaction
  5. churn wants segments, not surgery

basics

~20 s

Most implementations tombstone deletes: the vector stays in the graph as a connector and is only filtered out of results. Queries then spend their candidate budget on dead nodes, so latency rises and effective recall falls until compaction or a rebuild.

solid answer

~50 s

HNSW cannot cheaply remove a node, because its edges are load-bearing — they are part of the paths other queries walk. So implementations mark the node deleted and keep traversing it, excluding it only from the returned results. That works fine at small deletion ratios and degrades steadily as they grow. A nightly purge of 2M expired listings from an 80M-row catalogue leaves the graph the same size, but every query now spends part of its beam expanding nodes that can never be returned, so p99 latency drifts up and recall at a fixed efSearch drifts down. The mitigations, in order of cost: over-fetch and filter, raise efSearch to compensate, reuse tombstoned slots for new inserts where the implementation supports it, and ultimately compact or rebuild. Systems with heavy churn usually stop fighting this and build immutable segments that are merged in the background, or partition by time so an expired partition is dropped whole.

go deeper

for a junior

Know that deleting from a vector index usually just marks the entry rather than removing it, and that the space and the work are only reclaimed later when the index is rebuilt.

for a middle

Explain why removal is unsafe — the node's edges are paths other searches use, so dropping them can disconnect the graph — and that tombstoned nodes are still traversed and filtered only from results.

for a senior

Diagnose the symptom set: p99 drift at flat traffic, falling recall at fixed efSearch, short result counts, memory that never returns. Know the mitigation ladder and set a rebuild trigger on a measured tombstone ratio.

for a principal

For a high-churn corpus, argue the architecture rather than the tuning: immutable segments with background merges, or time-partitioned indexes that are dropped whole. Frame it as converting an unbounded degradation into a bounded, continuously-paid cost.

## Why a graph index cannot just delete a node In a hash index or a B-tree, removing an entry is local. In a proximity graph it is not. Each node's edges are paths that other searches traverse — a node sitting between two dense regions may be the only bridge between them at layer 0. Physically removing it and dropping its edges can disconnect part of the graph, making a region unreachable from the entry point and silently destroying recall for queries that land near it. Repairing properly means re-running the neighbour-selection heuristic for every node that pointed at the deleted one, which is close to the cost of inserting them all again. So implementations take the pragmatic route: **tombstoning**. The node is flagged deleted. Traversal still walks through it and still uses its edges as connectors. The only change is at the end, when results are assembled: tombstoned nodes are filtered out and never returned. ## How the degradation actually shows up The failure is gradual and easy to misdiagnose, because nothing errors. **Latency drifts up.** The beam has a fixed width. Every slot occupied by a tombstoned node is a distance computation and a memory access spent on something that cannot appear in the answer. As the tombstone ratio climbs, more of each query's work is wasted, and p99 rises first because it is the queries that traverse dense deleted regions that suffer most. **Effective recall drifts down.** At a fixed efSearch, the beam holds fewer live candidates, so the true neighbours are more likely to fall out of it. The index's recall against live documents degrades even though the graph over the surviving vectors is untouched. **Result counts go short.** A request for the top 10 that over-fetches only slightly can come back with fewer than 10 live results once a chunk of the neighbourhood is dead, which surfaces as a product bug well before anyone suspects the index. **Memory does not come back.** Tombstoned vectors keep their payload and their adjacency lists. A catalogue that has churned through several times its live size can be paying for a multiple of the memory it needs. Concretely: purge 2M expired listings a night from an 80M-row catalogue and, without reclamation, within a few weeks a substantial fraction of the graph is dead weight, with p99 up and no single deploy to blame. ## The ladder of mitigations **Over-fetch and filter.** Ask for more than k and drop the tombstoned entries. Cheap, immediate, and only a band-aid — it does not recover the recall lost from a beam clogged with dead nodes. **Raise efSearch.** Widening the beam restores some of the live-candidate count. It costs latency, and it is paying twice for work you already own. **Slot reuse.** Some implementations let a new insert take over a tombstoned node's slot, rewiring its edges to the new vector. This keeps the node count flat under steady-state churn — deletes and inserts cancel out — and is the best answer for a corpus with roughly balanced turnover. It does not help a corpus that is net shrinking. **Compaction or full rebuild.** Build a fresh graph containing only live vectors, then swap it in. This is the only mitigation that actually recovers both the memory and the traversal quality, and it costs a full build. Trigger it on a measured tombstone ratio — say, when dead nodes exceed 20-30% — rather than on a fixed schedule, so the cost tracks the actual churn. ## Designing the problem away Systems with high churn generally stop treating deletion as an index operation at all. **Segmented, immutable indexes.** Write to small immutable segments, each with its own graph, and query all of them with a merge of the results. Deletes are recorded in a per-segment liveness bitmap, and a background merge periodically rewrites several segments into one, dropping the dead entries as a side effect. This turns an unbounded degradation into a bounded, continuously-paid background cost — the same pattern log-structured storage engines use, applied to vectors. **Time partitioning.** If deletion correlates with age — expiring listings, retention windows, rolling event data — shard the index by time period. Expiring a period means dropping an entire index, which is free, and the live shards are never polluted. ## Monitoring Whatever the strategy, the tombstone ratio must be an exported metric, and recall should be sampled periodically against a brute-force ground truth over the live set. Without both, this failure mode is invisible until it is a latency incident, and the first instinct — raise efSearch — treats the symptom while the cause keeps growing. ## What interviewers listen for The insight is that deleted nodes remain part of the graph because their edges carry other people's traffic, and that the cost is therefore paid on every query rather than at delete time. Everything else follows: the metric to watch, the rebuild trigger, and the architectural move to segments or time partitions when churn is a permanent feature of the workload.

  • Why not just unlink a deleted node and reconnect its neighbours to each other?
    Because reconnecting correctly means re-running the neighbour-selection heuristic for every node that pointed at it, which approaches the cost of reinserting them. Doing it naively — wiring the orphaned neighbours to each other — degrades the diversity of the edge set that keeps the graph navigable, and can leave a region reachable only through a long chain. Tombstoning defers that cost to a batch rebuild, where it is paid once at a good rate.
  • What metric tells you it is time to rebuild rather than keep tolerating tombstones?
    The tombstone ratio — dead nodes over total nodes — is the primary trigger, typically actioned somewhere in the 20-30% range, alongside sampled recall over the live set measured against a brute-force ground truth. Watch p99 latency at a fixed efSearch as the confirming signal: if it drifts up while corpus and traffic are flat, the beam is being spent on dead nodes. Rebuild on the measured ratio, not on a calendar.
  • How does a segmented index change the delete story?
    It converts an unbounded degradation into a bounded background cost. Each segment is an immutable graph with a liveness bitmap; deletes flip bits, and a background merge rewrites several segments into one, dropping dead entries as a by-product. No segment ever accumulates tombstones indefinitely, memory is reclaimed continuously, and the merge is a tunable background job rather than a full-index rebuild event.
  • A query for the top 10 starts returning 7 results. What is your first hypothesis?
    That tombstones are eating the result set: the search returned its k candidates, filtering removed the deleted ones, and the over-fetch margin was too small to cover them. The immediate fix is to over-fetch well past k and filter, but the real diagnosis is the tombstone ratio in that region of the graph — a short result count usually means the local neighbourhood has been heavily purged and the index needs compaction.

saying these in an interview costs you the question

  • Assumes a delete physically removes the node immediately
  • Thinks tombstones free the vector's memory
  • Says deleted nodes are skipped during traversal
  • Treats rising p99 as a hardware problem, not index rot
  • Plans rebuilds on a schedule while ignoring churn rate

context