skip to content

Why does an HNSW index need more RAM than the vectors alone, and how much more?

level: seniorimportance: should knowfreq 52%

answer

  1. the graph is data too
  2. neighbour ids per node, per layer
  3. layer 0 gets about twice M
  4. linear in M, linear in corpus size
  5. random pointer chasing hates disk

basics

~20 s

HNSW stores a neighbour-id list per vector per layer — about 2M ids at layer 0. At M=32 that adds roughly 250 bytes per vector, so an 80M-vector catalogue pays around 20 GB for graph edges alone, on top of the vectors.

solid answer

~50 s

The graph is data. Every vector carries an adjacency list at each layer it appears in — up to about 2M neighbour ids at layer 0 and M above it — and each id is typically a 4-byte integer. At M=32 that is roughly 256 bytes per vector for layer 0, plus a small tail for the exponentially rarer upper layers. On an 80M-row product catalogue that is about 20 GB of edges; doubling to M=64 doubles it to roughly 40 GB, which is usually the deciding factor rather than any recall gain. That sits on top of the vector payload itself, and both need to be resident: traversal is a chain of pointer-chasing random accesses, so a graph paged to disk collapses latency. This is why HNSW is the RAM-hungry corner of the index triangle, and why very large corpora get sharded across machines or moved to a family of index that compresses the payload.

code

python · 9 lines
python
n_vectors = 80_000_000
M = 32
id_bytes = 4
layer0_slots = 2 * M          # layer 0 typically allows about 2M links
edge_bytes = layer0_slots * id_bytes
print(n_vectors * edge_bytes / 1e9, "GB of layer-0 edges")

for m in (16, 32, 64):
    print(m, n_vectors * 2 * m * id_bytes / 1e9)

go deeper

for a junior

Know that HNSW stores the connections between vectors as well as the vectors themselves, so the index is bigger than the raw embeddings, and that it is designed to live in RAM.

for a middle

Explain that each node keeps a neighbour-id list per layer, about twice M slots at layer 0, at roughly four bytes per id — and that the edge memory therefore scales linearly with both M and the number of vectors.

for a senior

Produce the sizing arithmetic on demand for a real corpus, and explain why the graph must be resident: traversal is dependent random access with no prefetch or batching. Remember headroom for the double-copy window during a rebuild.

for a principal

Own the decision at the architecture level: HNSW buys recall and latency with RAM, so past a corpus size the choice is sharding, a compressing index family, or a disk-native graph design. Argue it from cost per query, not from index preference.

## Two things are stored, not one A common sizing mistake is to budget an index at the size of the vectors. For HNSW that is only half the bill. The index stores the vectors *and* the graph over them, and on a high-M configuration the graph can approach or exceed the payload it indexes. **The vector payload.** Each vector is held so that distance can be computed against it during traversal, in whatever numeric format the store uses. **The graph.** Each node holds an adjacency list per layer it belongs to: a list of neighbour identifiers, conventionally 4-byte integers, plus a small header for the list length. Layer 0 is the expensive one because every vector is in it, and because implementations typically allow roughly twice M links there — the bottom layer needs denser connectivity since it is where the fine-grained search happens. ## The arithmetic Take M = 32 on an 80M-vector product catalogue. Layer 0 allows about 2M = 64 neighbour slots per node, at 4 bytes each, so about 256 bytes per vector. Across 80M vectors that is roughly 20 GB. The higher layers add a geometric tail: only a small fraction of nodes reach layer 1, a small fraction of those layer 2, so the whole hierarchy above layer 0 adds a modest percentage rather than another multiple. Now M = 64. Layer 0 allows about 128 slots, roughly 512 bytes per vector, so about 41 GB of edges. The recall improvement from that doubling is usually a couple of points at a fixed beam width — real, but bought with 20 GB of RAM that could have gone to a second shard or to headroom for a rebuild. In practice, on this kind of catalogue, the M decision is made by the memory budget and validated by a recall sweep, not the other way round. Two caveats on the numbers. Adjacency lists are usually allocated at their maximum size rather than at actual degree, so the figure is a ceiling that is also close to the typical case. And implementations differ in id width and per-node overhead, so treat the arithmetic as a sizing estimate to be confirmed against the actual process RSS after a build on a sample. ## Why it has to be resident The reason this memory is hard to economise on is the access pattern. A graph traversal reads one node's neighbour list, computes distances against those neighbours' vectors, then jumps to whichever it picks — a chain of dependent random accesses with no locality to exploit. There is no sequential scan to prefetch and no batch to amortise. Every one of those hops that becomes a page fault costs orders of magnitude more than the distance computation it was serving, and a query makes hundreds of hops. So HNSW is designed around a memory-resident graph. Where the corpus outgrows one machine, the usual answers are to shard it across nodes and fan out queries, or to move to a different index family whose payload compresses aggressively, or to a graph index explicitly engineered for SSD-resident layouts, which restructure the graph to make traversal contiguous rather than random. Those are architecture changes, not tuning. ## Sizing in practice A sane sizing exercise for a large index: estimate vector bytes, estimate edge bytes as roughly 2M ids per vector at 4 bytes, add both, then add headroom. The headroom matters more than people expect. A rebuild-and-swap deployment holds two copies of the index at once, which briefly doubles the requirement; deletes that are tombstoned rather than reclaimed keep paying for vectors nobody can retrieve; and the process needs room above the index for query working sets and the allocator's fragmentation. It is also worth noting what is *not* free to change. Because M is baked into the graph at insert, discovering after the fact that the index does not fit the box is not a config fix — it is a rebuild at a lower M, with the recall loss that implies. ## Where this sits in the tradeoff Every ANN index picks a corner of the recall-latency-memory triangle. HNSW's corner is high recall at low latency, paid for in RAM. That is an excellent trade at tens of millions of vectors on a machine with enough memory, and a bad one at billions of vectors or on a tight per-tenant memory budget. Being able to say that plainly — and to produce the per-vector byte figure that makes it concrete — is what separates a sizing answer from a preference. ## What interviewers listen for They want the mechanism (adjacency lists are stored data), the rough arithmetic (about 2M four-byte ids per vector at layer 0), the scaling behaviour (linear in M, linear in corpus size), and the reason it cannot be paged out (random dependent access). A candidate who also mentions the double-copy headroom during a rebuild has clearly operated one.

  • Why can't you just page the HNSW graph to SSD and keep only hot nodes in RAM?
    Because traversal is a chain of dependent random reads with no locality: each hop's target is only known after the previous hop's distances are computed, so nothing prefetches and nothing batches. A query makes hundreds of such hops, and any that fault to disk dominate the latency budget. Disk-resident graph indexes exist, but they restructure the graph and its layout specifically to make traversal contiguous — you cannot get there by swapping out a standard HNSW index.
  • How much extra memory do the layers above layer 0 add?
    Comparatively little. Level assignment is exponentially decaying, so only a small fraction of vectors appear in layer 1, a small fraction of those in layer 2, and so on. Those layers also allow about M links rather than 2M. The whole hierarchy above layer 0 typically adds a modest percentage to the edge budget rather than another multiple, which is why sizing estimates are usually written against layer 0 alone.
  • You have sized the box for the index exactly. What have you forgotten?
    Headroom. A rebuild-and-swap deployment holds the old and new indexes simultaneously, roughly doubling the requirement for the duration. Tombstoned deletes keep occupying memory until compaction reclaims them. And the process needs room above the index for query working sets, thread stacks and allocator fragmentation. Sizing to the exact index footprint means the first rebuild takes the service down.
  • Given a fixed memory budget, how do you decide between a higher M and more vectors?
    Measure both ends. Run a recall sweep at the candidate M values on a sample to see what the extra edges actually buy — often a couple of points at fixed beam width, sometimes less. Then weigh that against what the same RAM buys elsewhere: more of the corpus indexed, a second replica for availability, or headroom for rebuilds. On most catalogues the recall gain from doubling M is smaller than the value of the memory it consumes.

saying these in an interview costs you the question

  • Sizes the index at the vector bytes only
  • Thinks the graph can be stored on disk without penalty
  • Assumes M affects recall but not memory
  • Believes upper layers dominate the edge footprint
  • Forgets that a rebuild needs two copies resident

context