AI, LLM & Machine Learning Engineering RAG, Embeddings & Vector Search HNSW graph index (Malkov & Yashunin, 2016)

Vector Database Storage Calculator

This calculator sizes a vector index the way it actually consumes memory: the raw embedding bytes, plus the neighbour lists an HNSW graph stores alongside every vector, plus your metadata payload, plus a general overhead allowance, multiplied by replicas. The raw arithmetic — dimensions times bytes per component — is the part everyone does and the part that understates the answer, because a graph index is not free and metadata is often larger than people assume.

Calculator

This calculator runs in your browser. Enable JavaScript for live results — the inputs, formula and worked example below remain fully readable without it.

Inputs this calculator takes, with typical values
InputWhat to enterExample
Number of vectorsOne vector per indexed chunk. If you chunk documents, this is the chunk count, not the document count.5000000 vectors
Embedding dimensionsOutput length of your embedding model — 384, 768, 1024, 1536 and 3072 are the common values.1536 dims
Stored precisionHow each component is stored in the index. Quantisation shrinks the vector bytes and changes recall, which you have to measure on your own data.fp32 — 4 bytes per component
HNSW connections per node (M)The M parameter of the graph index. Each node stores about 2M 4-byte neighbour ids. Enter 0 for a flat (brute-force) index with no graph.16 links
Metadata per recordStored payload per vector: source id, title, timestamps, filter fields. Include the chunk text here if your store keeps it alongside the vector.256 bytes
Additional overheadAllowance for id maps, deleted-record tombstones, filter indexes and allocator fragmentation, as a percentage of the bytes above.15 %
ReplicasFull copies of the index held for availability or read throughput. Shards divide one copy and do not change the total.1 copies

It returns

  • Total index size — All replicas, all components. 1 GiB = 2^30 bytes.
  • Bytes per vector, all in
  • Raw embedding data
  • HNSW graph links
  • Metadata payload
  • Additional overhead
  • RAM to serve it in memory — Index size plus a 25% working allowance — a planning rule of thumb, not a measured figure.

The formula

S=n(db+8M+m)(1+h)r
1 GiB=230=1073741824 bytes

In plain text: Total = n · (d·b + 8M + m) · (1 + h) · r

  • STotal index size (bytes)
  • nNumber of vectors (count)
  • dEmbedding dimensions (count)
  • bBytes per stored component (4, 2, 1 or 0.125) (bytes)
  • MHNSW connections per node; 8M bytes covers 2M neighbour ids of 4 bytes each (count)
  • mMetadata payload per record (bytes)
  • hAdditional overhead fraction (fraction)
  • rReplicas (count)

The 8M term follows the per-element memory model published with hnswlib, which gives roughly (d·4 + M·2·4) bytes per element for fp32 vectors. Layer-0 nodes hold up to 2M links and higher layers hold M, with the expected number of higher-layer nodes small, so 2M is the working figure.

Updated Category RAG, Embeddings & Vector Search Verified against published test cases Reading time 10 min

What actually occupies memory in a vector index

A vector index stores four things, and the one people compute is usually the smallest surprise. The raw vectors are dimensions times bytes per component: a 1,536-dimension fp32 embedding is 6,144 bytes, and a million of them is 6.14 GB. That much is arithmetic.

The graph is the part that is easy to forget. Approximate nearest-neighbour search at scale is done with HNSW, a navigable small-world graph in which every vector is a node holding a list of neighbour ids. Those lists are stored, and they are not small: at the common setting M = 16, each node keeps up to 2M = 32 four-byte ids, or 128 bytes. Against a 1,536-dimension fp32 vector that is only 2%, so it disappears into rounding. Against a 384-dimension int8 vector it is a third of the record. Quantise aggressively and the graph, not the data, becomes your dominant cost.

The metadata is whatever payload you attach: source id, title, URL, timestamps, tenant id, and any field you filter on. Teams that store the chunk text itself alongside the vector often find the payload is several times the size of the embedding — an 800-token chunk is roughly 3,200 bytes of UTF-8 text, half again the size of a 1,536-dimension int8 vector.

The overhead covers id maps, deleted-record tombstones that persist until compaction, secondary indexes on filter fields, and allocator fragmentation. It is implementation-specific, which is why it is an input here rather than a constant.

The formula, term by term

d · b is the vector itself. b is 4 for fp32, 2 for fp16 or bf16, 1 for int8 scalar quantisation and 0.125 for binary quantisation, where each component collapses to a single bit. Note that quantisation acts only on this term.

8M is the graph. The per-element memory model published with hnswlib gives roughly (d·4 + M·2·4) bytes per element, which is where the coefficient comes from: 2M neighbour ids of 4 bytes each. HNSW is hierarchical, so nodes also appear in higher layers with M links rather than 2M, but the expected number of higher-layer nodes falls geometrically and contributes a small correction. Raising M improves recall and raises both memory and build time; setting it to 0 here models a flat index that stores no graph at all and scans every vector per query.

The (1 + h) factor multiplies everything, including the graph and metadata, because fragmentation and bookkeeping scale with the records rather than with any single component. The replica factor r multiplies again. Note the distinction from sharding: shards divide one copy of the index across nodes and leave the total unchanged, while replicas are full copies and multiply it. A three-shard, two-replica deployment holds six partitions totalling twice the index size.

Everything is reported in GiB, meaning 230 = 1,073,741,824 bytes. Cloud providers usually bill storage in GB of 109 bytes, which is 7.4% larger a unit, so a 5.84 GiB index is 6.27 GB on an invoice. Check which one a quoted limit uses before sizing to it.

Worked example: 5 million vectors at 1,536 dimensions

You are indexing 5,000,000 chunks with a 1,536-dimension model, stored as fp32, in an HNSW index with M = 16, with 256 bytes of metadata per record, a 15% overhead allowance, and one replica.

  1. Vector bytes. 1,536 × 4 = 6,144 B.
  2. Graph bytes. 2 × 16 × 4 = 128 B.
  3. Metadata. 256 B.
  4. Subtotal per record. 6,144 + 128 + 256 = 6,528 B.
  5. With overhead. 6,528 × 1.15 = 7,507.2 B per record.
  6. Total bytes. 7,507.2 × 5,000,000 = 37,536,000,000 B.
  7. In GiB. 37,536,000,000 ÷ 1,073,741,824 = 34.96 GiB.
  8. RAM to serve it. 34.96 × 1.25 = 43.70 GiB, using a 25% working allowance as a planning rule of thumb.

Broken down, that is 28.61 GiB of embedding data, 0.60 GiB of graph links, 1.19 GiB of metadata and 4.56 GiB of overhead. The graph is 1.7% of the index here.

Now switch the stored precision to int8 and hold everything else. Vector bytes fall to 1,536, the subtotal becomes 1,536 + 128 + 256 = 1,920 B, with overhead 2,208 B, and the total is 11,040,000,000 B = 10.28 GiB. The vector data alone shrank by exactly 4×, but the index shrank by 34.96 ÷ 10.28 = 3.40×, because the graph, the metadata and the overhead computed on them did not shrink at all. That gap is the reason to size quantisation from the whole record rather than from the vector.

How to read the result and where it stops fitting

Compare the RAM figure against the machine you intend to run on, because HNSW is a memory-resident structure by design: the search walks the graph following pointers, and every hop that lands on disk costs a random read. An index that does not fit in RAM does not degrade gracefully, it falls off a cliff. The 25% working allowance above is a planning rule of thumb covering query buffers, the operating system's own needs, and the transient extra memory a segment merge or compaction requires; it is not a measured constant, and a store that rebuilds segments in place may need considerably more.

When the total outgrows a node, you have three levers and they are not equivalent. Sharding divides the index across machines and keeps everything in memory, at the cost of querying every shard and merging results. Quantisation shrinks the vectors in place, and the worked example shows the whole-index saving is always less than the per-vector saving. Disk-backed indexes keep the graph and a compressed representation resident while the full-precision vectors live on SSD, re-ranking the shortlist with a small number of reads; this is the approach that scales furthest per node, and it trades a little latency for a large amount of memory.

Look at which component dominates before choosing. If metadata is the largest term, none of the three helps and the answer is to move the payload out: keep an id in the vector store and fetch the text from a document store after retrieval. If the graph dominates — which happens with low dimensions and aggressive quantisation — lower M, or use a clustered index such as IVF that stores centroids rather than per-node neighbour lists.

Finally, size for growth. The vector count comes from your chunking decisions, and halving the chunk size roughly doubles it. Work the two together with the RAG chunking calculator before committing to hardware.

GiB per million vectors by dimension and precision

Each cell is (d × b + 128) × 1,000,000 ÷ 230, with the 128 bytes being an HNSW graph at M = 16. No metadata or overhead is included, so add your own payload on top.
Dimensionsfp32fp16int8binary
3841.550.830.480.16
7682.981.550.830.21
1,0243.932.031.070.24
1,5365.842.981.550.30
3,07211.565.842.980.48

Read down a column to see the cost of dimensionality, and across a row to see the cost of precision. The binary column flattens because the fixed 128-byte graph dominates once the vector is only 48–384 bytes.

Sizing mistakes that show up in production

  • Counting documents instead of chunks. The vector count is the chunk count. A 5,000-document corpus split at 512 tokens is commonly 15,000–20,000 vectors.
  • Omitting the graph. At 1,536 fp32 dimensions it is 2% and forgivable; at 384 int8 dimensions it is 25% of the record and changes the machine you need.
  • Storing chunk text as metadata without counting it. An 800-token chunk is roughly 3,200 bytes — larger than a 1,536-dimension int8 vector, and it is stored on every replica.
  • Confusing shards with replicas. Shards split one copy; replicas multiply it. Only replicas change the total in this calculation.
  • Mixing GB and GiB. Cloud storage is usually billed in GB of 10^9 bytes, 7.4% larger a unit than a GiB. A 100 GiB index is 107.4 GB on the invoice.
  • Sizing to exactly the index size. Building or rebuilding an index needs transient headroom, and deleted records are not reclaimed until compaction runs.
  • Assuming quantisation preserves recall. It usually costs some, and how much depends on your model and your data. Measure recall@k against an exact search on a query sample before shipping it.

How this compares with other index types and where the cost really sits

HNSW is the default in most vector stores because it gives high recall at low query latency, and this calculator assumes it. Two alternatives change the memory profile materially. IVF partitions vectors into clusters around centroids and searches only the nearest few, storing centroids rather than a neighbour list per node — substantially less index overhead, at the cost of recall that depends on how many partitions you probe. Product quantisation compresses each vector into a short code of subspace centroid ids, often 32–64 bytes regardless of dimension, which is a far larger saving than scalar quantisation and a correspondingly larger hit to precision; it is normally paired with a re-ranking pass over full-precision vectors.

Managed vector services price on their own units — storage tiers, pod sizes, read and write units — and rarely expose the bytes directly. The figure this page produces is still the right input, because it tells you which tier your data lands in and whether a quantisation or dimension change would move you down one.

Note also that dimensionality is a choice, not a given. Several current embedding models are trained so their vectors can be truncated to a shorter prefix with a modest quality loss, which turns a 3,072-dimension model into a 1,024-dimension one and cuts the vector bytes by two thirds without re-embedding at a different provider — though it does require a full re-index, whose cost the embedding cost calculator prices.

If you are weighing this against serving a model on the same hardware, note that vector index memory and model weights compete for the same GPU or host RAM. The GPU VRAM requirement calculator and the quantisation memory savings calculator size that side, and the same quantisation logic — a fixed component that does not shrink — applies there too.

Frequently asked questions

Does the vector index have to fit in RAM?

For a classic HNSW index, effectively yes. Search traverses the graph by following pointers between nodes, and each hop is a random access; served from disk, those become random reads and latency rises by orders of magnitude. Disk-backed designs exist and work well, but they change the structure — keeping the graph and a compressed vector representation in memory and reading full-precision vectors only for a small re-ranking shortlist.

How much does the HNSW graph really add?

About 8M bytes per vector, which is 128 bytes at the common M = 16. As a share of the record it depends entirely on the vector size: 2% of a 1,536-dimension fp32 vector, but 25% of a 384-dimension int8 vector. Raising M to 48 triples it to 384 bytes and buys higher recall. The graph is also why aggressive quantisation returns less than its headline ratio.

Will int8 quantisation cut my index by four times?

It cuts the vector bytes by four times, and the index by less. In the worked example above, 1,536-dimension records fall from 34.96 GiB to 10.28 GiB — a factor of 3.40, not 4.00 — because the 128-byte graph, the 256-byte metadata and the overhead computed on them are unchanged. The smaller the vector relative to the fixed terms, the wider the gap.

What is a realistic overhead percentage?

It is implementation-specific, which is why it is an input rather than a constant here. Allow for id maps, deleted-record tombstones that persist until compaction, secondary indexes on any field you filter by, and allocator fragmentation. Fifteen percent is a reasonable planning starting point for a store with light filtering; heavy filtering on many fields, or a high delete rate between compactions, pushes it well above that. Measure your own once the index is live.

Do shards or replicas change the total?

Replicas do, shards do not. Sharding divides a single copy of the index across machines so that each holds a fraction; the sum is one index. Replicas are complete copies for availability and read throughput, so two replicas hold twice the data. A three-shard, two-replica deployment has six partitions and stores twice the index size in total.

Can I reduce dimensions instead of quantising?

Yes, and it saves more per unit of quality in many cases. Several current embedding models are trained so a truncated prefix of the vector remains usable, so a 3,072-dimension model can be stored at 1,024 dimensions for a third of the vector bytes. It requires re-indexing, and you should measure retrieval quality at the shorter length on your own queries. Quantisation and truncation compose — you can do both.

Why does the calculator report GiB rather than GB?

Because memory is allocated in binary units and index sizes are naturally expressed in them: 1 GiB = 230 = 1,073,741,824 bytes. Cloud storage and network products usually bill in decimal GB of 109 bytes, which makes the same data look 7.4% larger. Multiply the GiB figure by 1.0737 when comparing against a decimal quota.

How do I estimate metadata size per record?

Add up the fields you actually store, counting UTF-8 bytes: an id is 16–36 bytes, a URL 50–200, a title 50–100, timestamps 8 each, plus any JSON key names and structure. That lands most metadata-light designs at 150–400 bytes. If you also store the chunk text, add roughly 4 bytes per token — an 800-token chunk is about 3,200 bytes, which will usually dominate the record.

References