Embeddings are just arrays of numbers, which makes storing them sound like a solved problem. It is, but the default choices are wrong by an order of magnitude — and the mistake is almost always the same one: treating a dense numeric array as though it were a document.
The short version
An embedding is dimensions × 4 bytes in float32. A 1,536-dimension vector is ~6KB; a million of them ~6GB.
Store them binary and columnar — Parquet with metadata, NPY without. Never JSON, which inflates the same data three to four times and has to be parsed on every read.
The size arithmetic
Everything downstream follows from one multiplication, so it is worth having the numbers to hand.
| Precision | Bytes/dim | 1,536-dim vector | 1 million vectors |
|---|---|---|---|
| float32 | 4 | 6.1 KB | 6.1 GB |
| float16 | 2 | 3.1 KB | 3.1 GB |
| int8 | 1 | 1.5 KB | 1.5 GB |
| binary (1-bit) | 0.125 | 192 B | 0.2 GB |
| JSON text | ~13 | ~20 KB | ~20 GB |
The last row is the one that ruins projects, and it is worth seeing why rather than taking it on faith.
So JSON costs you roughly four times the disk, a great deal more memory during parsing, and load times measured in minutes rather than seconds. It is the natural choice if you think of an embedding as data to be transmitted, and the wrong one because an embedding is a fixed-width numeric array that never needs to be human-readable.
The formats, by situation
| Format | Good for | Weakness |
|---|---|---|
| NPY / NPZ | One array, one process | No metadata, no filtering |
| Parquet | Vectors + metadata at scale | Not a live index |
| Arrow / Feather | Fast interprocess sharing | Less compression |
| HDF5 | Huge scientific arrays | Heavy, fiddly concurrency |
| Safetensors | Model weights specifically | Not built for row data |
| CSV | Nothing here | Text overhead, no types |
| JSON | Nothing here | See above |
NPY is the right answer when you have one array and one program. It is a tiny header followed by the raw bytes, it memory-maps cleanly, and loading is effectively instant because there is nothing to decode.
Parquet is the default for anything real, because real systems never have only vectors. You have a document ID, a source, a timestamp, a chunk index — and Parquet stores those in columns alongside the embedding.
That columnar property is the practical advantage. Filtering, counting and inspecting your corpus are common operations, and in Parquet they never touch the expensive column.
⚠️ Set the float type explicitly
Many pipelines produce float64 by default — double the necessary size for zero retrieval benefit, because the embedding model did not generate that precision in the first place. You are storing rounding noise.
Cast to float32 as a minimum, and to float16 unless you have measured a reason not to. This is a one-line change that halves or quarters your storage, and it is the most commonly missed easy win in embedding pipelines.
Trading precision for space
Embeddings tolerate precision loss far better than intuition suggests, because retrieval depends on the relative ordering of similarity scores rather than their exact values. Small perturbations rarely reorder the top results.
The standard pattern at scale is two-stage retrieval: search a heavily quantised index to get a few hundred candidates fast, then rescore just those against full-precision vectors. You get most of the speed and storage benefit of quantisation with accuracy close to the full-precision baseline, because the aggressive stage only has to be good enough to keep the right answers in the candidate set.
When you actually need a vector database
Vector databases are excellent and frequently adopted a year before they are needed. The honest threshold is higher than the marketing implies.
Below roughly 100,000 vectors this is genuinely the right answer, and it has properties an approximate index does not: results are exact, there is nothing to tune, and there is no service to operate.
| Scale | Approach |
|---|---|
| < 10k | Array in memory, brute force |
| 10k – 100k | Array or an embedded index |
| 100k – 10M | Local ANN index, or a vector DB |
| > 10M | Vector database |
| Any scale, live writes | Vector database |
That last row matters more than the size thresholds. Static corpora are easy at any scale — build the index offline and serve it. Corpora with constant inserts, updates and deletes are where a database earns its keep, because maintaining a mutable approximate index correctly is genuinely hard and not something to build yourself.
🚨 Keep the source text, and keep it linked
An embedding is a one-way transformation. You cannot recover the text from the vector, so a store of vectors without their source text is a store of numbers nobody can interpret.
Worse, you cannot re-embed. When you change embedding model — and you will, they improve — every existing vector becomes incompatible and the entire corpus must be regenerated from the original text. Store the text, the chunk boundaries, and which model and version produced each vector. That last field is the one everyone omits and everyone later needs.
A storage layout that survives contact with reality
Three properties make this hold up over time:
- Text and vectors are separate. Re-embedding writes a new file; nothing else changes.
- The model version is a column, so mixed-generation corpora are detectable rather than silently broken.
- The index is disposable. Anything you can rebuild from the parquet files should not be your source of truth, and treating an index as durable state is how corpora become unreproducible.
If you are moving between formats during a migration, our JSON to CSV converter handles the metadata side in the browser — though for the vector columns themselves, a binary format is the destination you want.
Working with the metadata around your vectors?
Convert and validate JSON and CSV entirely in your browser — nothing is uploaded to a server.
Open JSON to CSV →Summary
- dimensions × 4 bytes is the float32 size. 1M × 1,536-dim ≈ 6GB.
- JSON inflates vectors three to four times and must be parsed on every read.
- Parquet for vectors plus metadata; NPY for a bare array.
- Cast to float32 or float16 explicitly — float64 defaults store rounding noise.
- Quantisation is cheap accuracy-wise; pair aggressive levels with a rescoring pass.
- Under ~100k vectors, brute force is exact and fast enough.
- Live writes justify a vector database more than raw scale does.
- Always keep the source text and the model version. You will re-embed.
Frequently Asked Questions
How much disk space do embeddings take?
As raw float32, it is dimensions times four bytes per vector — so a 1,536-dimension embedding is about 6KB, and a million of them is roughly 6GB. Halving precision to float16 halves that with negligible retrieval impact, and quantising to 8-bit integers cuts it to a quarter.
Why shouldn't I store embeddings as JSON?
Because every float becomes a text string of up to 20 characters instead of 4 bytes, inflating storage roughly three to four times, and every read has to parse text back into numbers. A million vectors that occupy 6GB in binary can exceed 20GB as JSON, and load times go from seconds to minutes.
What is the best file format for storing embeddings?
For a single array you control, NPY is simple and fast. For anything with metadata alongside the vectors, Parquet is the practical default — it is columnar, compressed, widely supported and lets you filter without reading the vectors. Both beat JSON and CSV by a wide margin.
Do I need a vector database?
Below roughly 100,000 vectors, usually not — a brute-force search over an in-memory array is fast enough and far simpler to operate. Vector databases earn their complexity at larger scale, or when you need live inserts, metadata filtering and persistence without building those yourself.
Does reducing float precision hurt search quality?
Very little for float16, which is why it is a common default. More aggressive quantisation to 8-bit or binary trades measurable recall for large storage and speed gains, and is usually paired with a rescoring pass over full-precision vectors for the top candidates.