~/rivestack/bench $ _
Managed pgvector benchmarks.
QPS with recall. Not marketing.
// Real HNSW search numbers across all three dedicated tiers on local NVMe, measured per tier, per dataset size, per recall target, and per concurrency level. Every QPS figure states its recall@10 and its concurrency, because without those two numbers a throughput claim means nothing. All results are reproducible with the open-source pgvector-bench CLI.
Honest numbers · HNSW · dedicated NVMe · reproducible with pgvector-bench
bench> Solo · 1 vCPU / 2 GB · 250k × 1536
~983QPS · 4 clients · ef_search=80 · recall@10 0.93
3.8 ms
p50
6.6 ms
p95
8.3 ms
p99
// One measured operating point. Every number on this page is a (tier, dataset, ef_search, recall@10, concurrency, QPS, latency) tuple.
# key numbers
// One representative operating point per tier. Each cell shows the recommended recall-0.93 setting where available, or the largest-dataset row for tiers where a bigger index was measured. The p50/p95/p99 triple and the recall@k are always included, because they are what the benchmark actually measures.
Solo · 1 vCPU / 2 GB
$29/mo · 250k × 1536
~983
QPS · 4 clients · ef_search=80
3.8 ms
p50
6.6 ms
p95
8.3 ms
p99
Growth · 4 vCPU / 8 GB
$85/node · 250k × 1536
~2,949
QPS · 16 clients · ef_search=80
5.1 ms
p50
8.7 ms
p95
11.4 ms
p99
Scale · 8 vCPU / 16 GB
$159/node · 250k × 1536
~4,465
QPS · 16 clients · ef_search=80
3.4 ms
p50
5.3 ms
p95
6.6 ms
p99
# full results table
// All measured (tier, dataset, ef_search, recall@10, concurrency) combinations. Cells shown as "—" were not captured for that configuration. The Solo row at ef_search=200 / recall 0.99 is the known collapse point where the working set spills past the 4 GB cache and latency surges to 43 ms p50.
| tier | vectors | ef_search | recall@10 | clients | QPS | p50 | p95 | p99 |
|---|---|---|---|---|---|---|---|---|
| Solo | 250k × 1536 | 10 | 0.74 | 4 | 1,446 | 2.7 ms | 4.0 ms | 5.6 ms |
| Solo | 250k × 1536 | 80 | 0.93 | 1 | 712 lowest latency | 1.3 ms | 2.0 ms | 2.7 ms |
| Solo | 250k × 1536 | 80 | 0.93 | 4 | 983 | 3.8 ms | 6.6 ms | 8.3 ms |
| Solo | 250k × 1536 | 120 | 0.96 | 4 | 832 | 4.4 ms | 7.8 ms | 9.7 ms |
| Starter | 250k × 1536 | 10 | 0.75 | 16 | 2,088 peak throughput | 7.1 ms | 13.0 ms | 17.3 ms |
| Starter | 250k × 1536 | 80 | 0.93 | 4 | 1,185 | 3.2 ms | 4.8 ms | 6.0 ms |
| Starter | 250k × 1536 | 80 | 0.93 | 16 | 1,599 | 9.3 ms | 17.1 ms | 22.3 ms |
| Growth | 250k × 1536 | 10 | 0.82 | 16 | 3,961 peak throughput | 3.7 ms | 6.7 ms | 8.7 ms |
| Growth | 250k × 1536 | 80 | 0.94 | 4 | 1,989 | 1.9 ms | 2.7 ms | 3.2 ms |
| Growth | 250k × 1536 | 80 | 0.94 | 16 | 2,949 | 5.1 ms | 8.7 ms | 11.4 ms |
| Scale | 250k × 1536 | 10 | 0.80 | 16 | 5,815 peak throughput | 2.6 ms | 4.1 ms | 5.1 ms |
| Scale | 250k × 1536 | 80 | 0.95 | 8 | 3,323 | 2.3 ms | 3.1 ms | 3.8 ms |
| Scale | 250k × 1536 | 80 | 0.95 | 16 | 4,465 | 3.4 ms | 5.3 ms | 6.6 ms |
| Scale | 1M × 1536 | 80 | 0.74 | 16 | 3,600 1M index hot in RAM | 4.2 ms | 6.9 ms | 8.9 ms |
| Scale | 1M × 1536 | 200 | 0.85 | 16 | 2,410 1M, higher recall | 6.2 ms | 10.7 ms | 14.0 ms |
# build ceiling
// HNSW index builds are memory-bound. Each tier has a hard size limit beyond which the build takes hours or fails entirely. The throughput numbers above are irrelevant if the index will not build. Size the node by index build memory first, QPS second.
| tier (RAM) | builds fine | builds, but slow / unusable | will not build |
|---|---|---|---|
| Solo (2 GB) | 250k (~15 min) | 500k (RAM-limited) | 1M (OOM) |
| Starter (4 GB) | 250k (~5 min) | 500k (tight) | 1M (OOM) |
| Growth (8 GB) | 250k (~2 min) | ~500-750k | 1M (untested) |
| Scale (16 GB) | 250k (~80 s), 1M (~9 min) | — | — |
# methodology
// Exactly what was measured and how, for anyone who wants to understand or reproduce it.
Client and network path
The benchmark client ran on a separate VM in the same region (Hetzner EU, Falkenstein), connecting over the public endpoint with TLS (sslmode=require). That is exactly the path a same-region application takes. On Solo the client connects through PgBouncer (port 6432, transaction pooling); on Growth and Scale through the load balancer (port 5432). Bare SELECT 1 round-trip overhead from the same region is about 0.4 ms on Solo and 0.9 ms on Growth/Scale. Cross-region latency would add roughly 90 ms per query and would dominate all these numbers.
Dataset and index
Vectors are synthetic but clustered: a 1536-dimensional Gaussian mixture, normalized for cosine distance. A clustered mixture is used rather than uniform random noise so that recall is representative of real embedding structure, and real embeddings may recall slightly higher. The index is HNSW with m=16 and ef_construction=64 (the pgvector defaults for 1536-dim). Recall@10 is measured against exact brute-force KNN ground truth computed with index scans disabled.
Why recall and concurrency are non-negotiable
Approximate nearest-neighbor search trades recall for speed. The hnsw.ef_search parameter controls the tradeoff. A QPS number without a recall number and a concurrency count is an advertisement, not a measurement. Every result on this page is a (tier, dataset, ef_search, recall@10, concurrency, QPS, latency) tuple. A single headline number without those qualifiers tells you nothing about what the system actually delivers for your workload.
Reproducibility
All numbers were produced with the open-source pgvector-bench CLI, released under the MIT license and available at github.com/Rivestack/pgvector-bench. A single Go binary with an interactive wizard and a self-contained HTML report. Run it against any PostgreSQL instance (including a Rivestack free tier) to measure your own p50/p95/p99, QPS curve, and recall@k against brute-force ground truth on your actual data.
// related: NVMe vs cloud SSD benchmarks (full blog post) is the source of every number on this page, including the latency curves, the ef_search=200 collapse, and per-tier analysis. Run the benchmarks yourself with the pgvector-bench CLI. Try managed pgvector against a free tier to measure your own index.
# frequently asked questions