Benchmarks

The benchmarks measure three criteria: speed, accuracy and cost.

Benchmark infra

Everything is measured on an 8-vCPU Amazon EC2 instance (Ubuntu, us-west-2) with polign_db's benchmark harness, calling the database code directly. The cloud-storage tests ran against three stores: the local disk, MinIO (S3-compatible store on the same machine), and Amazon S3 in the same AWS region.

The 10-million-vector tier ran on a memory-optimized instance of the same generation (r7i.2xlarge, 8 vCPU / 64 GB); the 100-million-vector tier ran on 16 vCPU / 128 GB (r7i.4xlarge, us-east-2), with its cloud-storage tests against an S3 bucket in that same region.

Accuracy here means: of the 10 truly closest matches, what share did the search actually return (the standard measure called recall@10).

Note: this page covers the vector-search paths. Hybrid search (BM25 + rank fusion) and filtered search are shipped features but not benchmarked here yet.

The short version

The headline numbers primarily revolve around costs. About $3.20 per million searches served straight from cloud storage with no index in RAM. That's at the 8-region setting, which measured 84% accuracy on the 1M test; the 98%-accuracy setting checks 32 regions and costs about $12.80. And about $0.01 per million writes through a write log kept directly in the bucket, with no separate queue server (like Kafka) in the middle. When everything is hot, polign_db's in-RAM path answers in well under a millisecond, though the RAM-resident engines in the comparison below are faster still.

ScenarioMeasured result
Search from memory — 1M vectors, 128-dim (SIFT1M) 2,860 searches/sec at 96.5% accuracy; 950/sec at 99.4%
Search from memory — 1M vectors, 768-dim (cohere) ~505 searches/sec at 94.6% accuracy
Search from memory — 10M vectors, 128-dim (BigANN) 1,095 searches/sec at 95.6% accuracy; ~150/sec at 99.5%
Search from memory — 100M vectors, 128-dim (BigANN, 16-vCPU machine) 869 searches/sec at 95.5% accuracy; ~150/sec at 99.5%
Search straight from cloud storage — no index in RAM 18 ms typical, and 27 ms even in the slowest 1% of searches (p99), at 98% accuracy (same-machine store); ~144 ms typical, ~354 ms p99, against Amazon S3 in the same region
Search straight from cloud storage — 10M vectors 95.3% accuracy at 283 ms typical (32 regions); 78.9% at 118 ms (8 regions), against Amazon S3 in the same region
Search straight from cloud storage — 100M vectors 91.6% accuracy at 385 ms typical (32 regions); 85.1% at 215 ms (16 regions), against Amazon S3 in the same region
What a cloud-storage search costs ~$3.20 per million searches at 8 regions checked ($0.40 per million S3 downloads × 8; 84% accuracy on the measured 1M split, while the 98% setting checks 32 regions, ~$12.80). No RAM or idle servers on the bill
Compressed index — 32× smaller in RAM 97.4% accuracy with the re-check pass, ~330 searches/sec
Compressed index — 100M vectors, 16× smaller (16-vCPU machine) 98.8% accuracy with the re-check pass, ~300 searches/sec
Writes through the S3 write log (no queue server in the middle) 6,200 writes/sec per log at ~161 ms typical, ~286 ms p99 confirmation; ~$0.01 per million writes. That's the single-log worst case. The default deployment runs 16 logs, ~100,000/sec aggregate (arithmetic, not a separate measurement)

Comparison with other databases

Cold and warm, in the format the category publishes

Every object-storage-backed engine has a cold number and a warm number, and the honest way to read this table is that the cold column is the cost of not renting RAM. We publish ours in the same shape everyone else does rather than quoting only the warm figure:

EngineCold / uncachedWarm / cachedAs published
polign_db ~144 ms p50, ~354 ms p99 @ 98% recall (1M, same-region S3); ~80 ms at lower-accuracy settings; ~385 ms p50 @ 91.6% at 100M <1 ms in-RAM; 18 ms p50 / 27 ms p99 against a same-machine store this page, 8-vCPU EC2
turbopuffer p50 874 ms at 1M documents (~400–900 ms range) p50 14 ms (14–16 ms for cached namespaces) vendor docs
S3 Vectors sub-second ~100 ms vendor docs
LanceDB several hundred ms on object storage <100 ms p95 (on EFS) vendor docs
Chroma cold start on first query designed for 35–100 ms vendor docs

Read across the cold column and the numbers land in the same band, because they are all measuring the same physical thing: an object-storage round trip. Ours is measured end-to-end at a stated recall on a stated instance; several of the others are published as a range or a design target. Where the designs actually diverge is the warm column and what it costs to get there, which is the next table.

Scale-to-zero rehydration: what a cold fleet costs to bring back

If nodes are disposable, the number that decides your bill is not the first query's latency but how long a brand-new node takes to become useful, and how much of that is rebuild work versus a download. We have not found this published by anyone else in the category, so here is ours. A node boots by fetching a pre-built index generation from the bucket; it never rebuilds:

CorpusNode boot from a published generationBuilding that index from scratchRatio
10M vectors5.2 s from same-region S3 (3.3 s from local disk)18 min (8 vCPU)~208×
100M vectors36 s (measured on local disk)8.4 h (16 vCPU)~840×

The build runs once per published index version; every boot, restart, and scale-out after that reuses it. So the cost of scaling to zero overnight is one boot-time fetch per node in the morning, plus whatever cold queries run before the heat mechanism promotes the hot collections back into RAM. Two caveats: the boot-time fetch is single-threaded today, and parallelizing it is the next engine change; and the 100M boot figure is from local disk, so the same-region S3 equivalent will be somewhat higher.

The architectural comparison behind these benchmarks is detailed here - comparison page.

Search speed — in memory

Each curve below is a trade-off from fast-and-rough to slow-and-thorough.

Search speed — cloud-native storage

Polign cold search is executed with no index in memory at all. Each search downloads just the pieces of the index it needs from a storage bucket (S3, GCS, Azure Blob, MinIO) and searches them on the spot. That makes the cost of a search unusually easy to state, because cloud storage bills per download: a search that checks 8 regions of the index makes 8 downloads, and at S3's price of $0.40 per million downloads, a million such searches cost about $3.20.

How big each download is gets decided when the index is built: splitting the same data into fewer regions means bigger files but fewer downloads per search. Here is what three different splits of the same 1-million-vector dataset produced:

Cold search costs

The defaults are S3 list prices; downloads within the same AWS region carry no bandwidth charge, so bandwidth only matters for cross-region or internet clients.

Downloads per search
one file per region checked
Data downloaded per search
measured average file size
Accuracy at this setting
measured on the 1M-vector test
Cost per 1M searches
downloads only

Write speed and cost

In this setup, every write is appended to a log that lives in the storage bucket itself. When many writers are active at once, writes that queue up behind an upload already in flight all ride along in the next one. So a single upload can carry hundreds of writes.

That batching is the entire difference between 33 and 6,233 writes per second measured against the same S3 bucket. It also sets the cost: S3 charges $5.00 per million uploads, so the cost of a write is that price divided by how many writes share an upload.

The default deployment spreads writes across 16 logs, and because the logs never coordinate, throughput scales with the log count. The measured ~6,200/sec per log puts the 16-log default at roughly 100,000 writes/sec aggregate.

Concurrent write safety

Writers don't coordinate in advance. If two try to append at the same moment, the storage itself accepts one and rejects the other, and the loser simply retries at the next position. Nothing is ever lost or overwritten. A collision only costs one extra billed upload. The numbers below are two writer processes deliberately fighting over one log:

Compression. Smaller index, nearly the same answers

Instead of keeping every full vector, Polign can keep a compact fingerprint of each one, 8–64× smaller in the tests below (the headline numbers on this page use the 32× setting). Searching fingerprints alone gives up some accuracy, so an optional second pass re-checks the top candidates against the true vectors before answering. The charts show how much accuracy the fingerprints alone give up, and how much of it the re-check wins back.

Scale test with 10 million and 100 million vectors

This section reruns the same three paths, memory, cloud storage and compression, at 10× and 100× the data, using the 10M and 100M slices of the BigANN benchmark (SIFT1B). BigANN publishes official answer keys at exactly these sizes, so the accuracy numbers are verified against the same ground truth.

Both indexes scale the region count with the corpus: 4,096 regions at 10M, 16,384 at 100M.

The 10M tier ran on the same 8-vCPU class as everything above; the 100M tier needed a bigger machine, 16 vCPU / 128 GB (r7i.4xlarge). Build times: 7.3 minutes in memory at 10M, 115 minutes at 100M.

The cloud-storage indexes came to 6.6 GB (4,096 files) and 65.6 GB (16,384 files) respectively.

In short: at scale, search time is still bounded by the storage round trip. A one-region cold search costs ~80 ms median at 1M, ~87 ms at 10M, and ~96 ms at 100M. The median region file grew from ~0.6 MB at 1M to ~3.5 MB at 100M and the median barely moved, because S3's per-download first-touch price dominates the bytes.

Compression at scale: fingerprints are 8–64× smaller than the raw vectors, and the compressed tier checks a fixed 128 regions per search regardless of corpus size, so speed barely moves with scale: ~300 searches/sec for 32-byte codes at both 10M (8 vCPU) and 100M (16 vCPU), at 25 and 46 ms median. Deepening the re-check pool buys back the accuracy the fixed probe gives up: at 100M, 32-byte codes reach 98.8% with a 100-deep re-check and 16-byte codes 98.6% re-checking 500; at 10M the same settings reach 99.4%+. Two caveats: the bounded scan concedes the last point of accuracy (a wide probe hit 99.75% at 100M, but at 1/70th the speed), and 8-byte codes stay weak at scale, reaching just 91.1% at 100M even re-checking 500, their ranking too coarse for all the true neighbours to make the pool. With the full vectors in same-region S3 instead of RAM, re-check depth is fetch traffic: 50/100/200/500 finalists cost 26/52/104/255 MB per search for 89.3/96.7/99.3/99.7% accuracy at 0.46–2.5 s median (measured at 10M). The chart's data table has the full breakdown.

Serving from a compressed replica

A replica node holds only the fingerprints in RAM (1.6 GB of fingerprints, ~2 GB structure in total, at 100M); the full vectors stay in cloud storage, and each search re-checks its finalists by downloading only the few kilobytes it needs from inside each file.

Micro-benchmarks: transport and engine

Server on an 8-vCPU Amazon EC2 instance (Ubuntu, us-west-2); the client's 16 workers on a separate t3.small in the same availability zone. That's a network hop, so the server's CPU is all its own. dim=128, k=10. Illustrative, not guarantees, because your hardware will differ.

Transport overhead (10k×128, 16 workers)

Over http/grpc the server seeds its default IVF-over-HNSW backend, so the local rows show both that default (the apples-to-apples transport comparison) and the pure-HNSW in-process ceiling. The local rows run on the server instance itself, so they're in-process by definition.

PathWorkloadQPSp50p90p99
local (HNSW)search (in-process)~71,0000.11 ms0.13 ms0.17 ms
local (IVF default)search (in-process)~14,9000.53 ms0.61 ms20.7 ms
grpcsearch~10,6001.40 ms2.06 ms2.86 ms
httpsearch~8,6001.70 ms2.57 ms3.90 ms
httpmixed 80/10/10~5,4002.45 ms5.06 ms10.1 ms

gRPC beats HTTP/JSON by ~23% on throughput with a tighter tail (p99 2.86 vs 3.90 ms). That's protobuf + HTTP/2 vs JSON + HTTP/1.1. One oddity, deliberately left visible: the local IVF p99 (~21 ms) is worse than gRPC's, and it is not a loopback artifact. It persists on an otherwise idle machine. At in-process saturation (~15k QPS) the driving process shares the engine's GC, so stop-the-world pauses land in its own tail; the served paths run the same backend ~30% below that saturation and don't show it.

Engine hot path: allocation-free search (50k×128, all cores)

A matched before → after pair on the same 8-vCPU EC2 instance, isolating the search hot-path optimization, which is SIMD distance kernels plus a pooled, allocation-free search scratchpad (visit-version visited set). BenchmarkSearchParallel reports per-parallel-op wall time, so aggregate throughput ≈ 1e9 / ns·op.

Benchmarkns/op (before → after)≈ agg QPSB/opallocs/op
BenchmarkSearchParallel (hnsw)117,097 → 35,269 (3.3×)~8,500 → ~28,40093,185 → 981,162 → 1
BenchmarkServiceSearch (service)798,032 → 257,421 (3.1×)~1,250 → ~3,900651,086 → 3,83312,906 → 5

Stripping per-query allocation out of the engine (1,162 → 1 allocs/op on the raw index) is what lifts concurrent throughput: fewer allocations mean less GC work competing across cores, and the ~5–8× AVX2+FMA distance kernels compound the win. The service search calls straight into the engine, so it inherits both.

Scaling: HNSW vs IVF-PQ as the corpus grows

CorpusHNSW QPSIVF-PQ QPSGapHNSW p50IVF-PQ p50
20k~60,900~6,9008.8×0.13 ms1.06 ms
50k~50,300~4,45011.3×0.15 ms1.67 ms
100k~42,300~3,24013.0×0.18 ms2.30 ms
200k~31,400~1,91016.4×0.25 ms3.92 ms
HNSW vs IVF-PQ

HNSW wins on speed at any size that fits in RAM (p99 stays under ~0.4 ms). IVF-PQ's reason to exist is footprint: CodesOnly stores ~16 B/vector vs HNSW's ~840 B/vector at dim=128. Reach for it when the corpus no longer fits HNSW in memory.

How these numbers were produced

Everything on this page comes from polign_db's own benchmark harness, run on standard EC2 instances against local disk, MinIO, and same-region Amazon S3, over public datasets: SIFT1M, VectorDBBench's cohere_medium_1m, and BigANN subsets for the 10M/100M tiers. Each chart states its instance type, dataset, and configuration, and every chart has a "view data as table" toggle backed by the raw JSON reports, including median and worst-case timings for every point.