CapyDB/ docs
← All posts

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.

August 17, 2026CapyDB team

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:

p50p95p99
SELECT 10.57 ms0.67 ms0.75 ms
Indexed point-select0.70 ms0.81 ms0.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:

p50p95
Fresh connect + query, direct port25.7 ms46.3 ms
Fresh connect + query, pooled port15.3 ms18.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.