Benchmarks
Every server runs as a container against the same PostgreSQL instance and the same dataset, each pinned to its own CPU cores on a dedicated bare-metal host. These are the numbers the harness produced.
REST, against PostgREST
Requests per second, with median latency. Same request in each server's own dialect.
| Scenario | Postrust | PostgREST | Ratio |
|---|---|---|---|
| point lookup | 44,3241.1 ms | 10,9582.8 ms | 4.0x |
| 25-row page | 34,6331.4 ms | 10,4884 ms | 3.3x |
| filtered + ordered page | 32,7011.5 ms | 6,3066.6 ms | 5.2x |
| range filter on numeric | 30,8371.6 ms | 7,0996.1 ms | 4.3x |
| 25-row page + embed | 20,0932.4 ms | 2,15216.8 ms | 9.3x |
GraphQL, against Hasura
Postrust and Hasura are sent the same query text, byte for byte.
| Scenario | Postrust | Hasura | Ratio |
|---|---|---|---|
| single row by primary key | 32,1461.5 ms | 9,6505.1 ms | 3.3x |
| 25-row page | 17,4112.8 ms | 10,3794.7 ms | 1.7x |
| 25-row page + embed | 10,0924.9 ms | 8,0246.1 ms | 1.3x |
GraphQL, against PostGraphile
PostGraphile inflects field names its own way, so the query text differs. The request is the same.
| Scenario | Postrust | PostGraphile | Ratio |
|---|---|---|---|
| single row by primary key | 32,1461.5 ms | 14,1273.3 ms | 2.3x |
| 25-row page | 17,4112.8 ms | 9,0035.6 ms | 1.9x |
| 25-row page + embed | 10,0924.9 ms | 4,49810.9 ms | 2.2x |
How much to trust each number
Repeated whole runs on this host agree to 0.8% for Postrust, 3% for PostGraphile and 3.6% for Hasura. PostgREST is the outlier at 19.6%, so multiples quoted against it carry roughly ±10%. The direction of the differences is far outside that margin; the precise multiples are not, and are not quoted to more precision than they carry.
Running in a container costs about 8.6% against the same binary run natively — 7.3 of those points are Docker's network path rather than the container itself. It is charged to every server equally, so ratios are unaffected and absolute figures are understated by about that much.
Why the REST gap is as large as it is
Worth understanding before quoting the ratio. Three ways it could have been an artefact were tested and ruled out: nothing is cached, and both servers issue exactly one query per request (pg_stat_statements records 3,000 calls for 3,000 requests on each); the connection pools are 10 and 11; and the responses carry the same rows, the same Content-Range and within two bytes of each other.
What differs is the SQL. Postrust sends a plain parameterised SELECT and renders the JSON itself. PostgREST wraps the query in a CTE, computes a count, builds the JSON inside PostgreSQL with json_agg and reads three GUCs — on every request, whether or not a count was asked for or a function set a response header.
So a large part of the ratio is where each design does the work, and PostgREST paying per-request for generality these scenarios do not use. That is a real architectural difference and a fair thing to measure. It is not evidence that Postrust's HTTP layer is four times faster, and the figures should not be read that way.
What stops a bad run being published
Two checks, both of which have caught something real.
The targets must be answering the same question. Every scenario is fetched from every server before any of them is measured. Row counts must match — a difference there is never a dialect difference — and for REST the columns must match too. Until this existed the harness checked an HTTP status and nothing else, so one server returning a single row while another returned twenty-five would have passed and been measured as equal work.
The machine must have held still. The first measurement of a run is repeated as its last action, with everything else in between. If the two disagree by more than 3%, the run exits non-zero and its figures are not publishable. This matters more than it sounds: with a deliberately short 2,000-request window the same comparison reads 13.1x instead of 4.0x, from a run that otherwise looks perfectly healthy. The gate is what stands between that number and this page.
Why those checks exist
This page carried a withdrawn notice for a while, and no numbers at all. The figures that had been here were measured on a virtualised host where the same server, in the same container, measured seconds apart differed by about a factor of two — and whatever ran first was the most understated, which made run order a determinant of the published ratios. Everything below was ruled out at the time, each by direct test, and the mechanism was never identified.
Thermal throttling and measurement order
Each server was measured both first and second in an alternating design. No advantage follows the position.
Host load
The harness scored higher at load average 18 than at load 5, while measuring by hand held steady at both.
A cold database
The tables are 49 MB against 128 MB of shared_buffers, the fixtures ANALYZE, and the harness pre-warms with count(*).
The bulk load and checkpointing
Reloading 400,000 rows and forcing a CHECKPOINT moved nothing.
Server and database start-up
A restarted server reaches full throughput on its first sample; so does a restarted PostgreSQL.
Pausing the other containers
A run with a single target, where there is nothing to pause, is equally depressed.
The harness's own measurement code
Calling it directly on a settled stack returns the right answer, and replicating its full sequence by hand does too.
Docker's port forwarding
The same load generator over the forwarded path and from inside the container network agree.
It does not reproduce on bare metal with each component pinned to its own CCD. It was escaped rather than explained, which is exactly why the self-consistency gate is now part of the harness rather than something done by hand — it is what would notice if the effect ever returned. The full record is in scripts/BENCH-FINDINGS.md, and the method in docs/benchmarking.md.
Conformance is measured separately
The conformance reports measure whether two servers give the same answer, not how fast they give it. Read the conformance reports.