The p95 of not having a database API
A viral tweet celebrated going from 480 ms to 80 ms by dropping an HTTP data gateway for direct Postgres connections. We measured what the same path costs on CapyDB - and found a bug in our own benchmark while doing it.
The tweet
A migration anecdote made the rounds recently: a high-traffic endpoint went from p95 480 ms to p95 80 ms by replacing Supabase's PostgREST layer with direct database connections.
The interesting part is not that one stack was misconfigured or another is bad. PostgREST is a fine piece of software. The interesting part is the shape of the win: nothing about the queries changed. What changed is that requests stopped paying an HTTP gateway - JSON translation, per-request auth, connection churn, an extra network hop - to reach a database that was fast all along.
CapyDB doesn't have that layer, on purpose. The Postgres wire protocol is the API. Which raises an obligation: if we are going to smirk at gateway overhead, we should publish what our own path costs. So we measured it - on production nodes, through the same TLS proxy every customer uses, with certificate verification on, from a client colocated with the region. The harness is open source.
What one query costs here
Warm connection, 500 repeats per variant, exact percentiles:
| p50 | p95 | p99 | |
|---|---|---|---|
SELECT 1 | 0.57 ms | 0.67 ms | 0.75 ms |
| Indexed point-select | 0.70 ms | 0.81 ms | 0.85 ms |
That is the entire database-side budget of a "fetch one row" endpoint: sub-millisecond at p95, tail included.
The nastiest case an app can construct - open a brand-new connection, TCP + TLS + SCRAM handshake, run the query, for every single request:
| p50 | p95 | |
|---|---|---|
| Fresh connect + query, direct port | 25.7 ms | 46.3 ms |
| Fresh connect + query, pooled port | 15.3 ms | 18.5 ms |
Read that against the tweet: our worst-case pattern - a full cryptographic handshake per request, the thing every driver and pooler exists to avoid - still lands under the anecdote's post-optimization 80 ms. And the pooled port connects faster than the direct one, because the pooler is already holding authenticated server connections open for you. If you are on serverless or edge runtimes, that is your default.
Full seven-statement read-write transactions, for scale: p95 stays between 9.5 ms (single client) and 25.8 ms (32 clients pushing 2 300 transactions/sec on a standard business cell).
The bug we caught in our own harness
Publishing benchmark numbers means being honest about the harness, so here is ours.
Our noisy-neighbor scenario measures a victim cell's p99 while an aggressor cell on the same node runs pgbench flat-out. This campaign we added a paranoid check: parse the aggressor's own progress output and fail the run if the storm was not actually sustained.
It immediately fired. On pgbench 16, -d does not mean "database" - it means --debug, which floods stderr with per-client lines. Our harness spawned the aggressor with a stderr pipe nobody drained, so after 64 KB of debug output the aggressor silently stalled. Our previously-measured noisy-neighbor numbers had been benchmarking a victim against an aggressor that mostly wasn't there.
The honest, verified number - aggressor confirmed sustaining ~1 640 transactions/sec through the whole window: the victim kept 49 % of its solo throughput and its p99 went from 28 ms to 72 ms. That is proportional fair-share between equal cells, exactly what our per-cell CPU clamps are designed to produce - and it is not "noisy-neighbor-proof isolation", which we have never claimed for shared nodes. A neighbor cannot starve you or take the whole node; contention splits the node fairly.
We could have quietly shipped the flattering earlier numbers. A benchmark you cannot trust to report its own failures is marketing with axes, so the harness now hard-fails when a load generator dies, and the retraction is in the repo's report permanently.
The rest of the campaign
- Scale-to-zero wake: a paused cell answered its first query in p50 308 ms (worst of twelve: 358 ms). Idle-to-first-row is a pause, not a cold boot.
- Preview creation: branching a 1 GB cell took 15.6 s; 256 MB took 10.2 s. Copy-on-write means creation time tracks orchestration, not data size.
- Pooled vs direct under load: transaction pooling gives up 10-20 % peak throughput and returns a smoother tail at high concurrency. That is the trade, measured.
All numbers, methodology, caveats, and reproduction steps live on the benchmarks page. If you run capybench against us - or against anyone else - from a properly colocated client, we would genuinely like to see the results.
The point
Nothing above is a heroic optimization story. There is no cache, no read replica, no edge trickery in those tables - just Postgres, TLS, and the absence of a gateway between your code and it.
That absence is the product decision. You do not migrate to direct connections after a painful p95 investigation; you start there, with every Postgres driver, ORM, and tool working unmodified - and later, when some dashboard shows a spike, the database is one less layer to suspect.
A small product surface is a feature
Every layer between you and Postgres is another thing to learn, debug, or rip out later.
Webhooks, integrations, and a real API surface
The product grew the connective tissue everything else plugs into - signed webhooks, Vercel and Netlify integrations, Clerk user sync, project-scoped keys, and an import preflight that says no before anything breaks.