DocsBenchmarks

Benchmarks

Every performance number we publish comes from a script in the repository. Run it yourself and check.

Reproduce it

The harness is self-contained: it builds a release binary, starts a throwaway PostgreSQL container, loads 100,000 rows, runs each scenario and prints a table. Everything it created is removed on exit.

git clone https://github.com/postrust/postrust
cd postrust
./scripts/bench.sh

# Heavier load
REQUESTS=20000 CONCURRENCY=100 ./scripts/bench.sh

# Measure a minimal build instead of the shipped feature set
BENCH_FEATURES="" ./scripts/bench.sh

Requires docker, cargo, curl, jq, and either oha or ab as the load generator.

Comparing against other tools

A second harness measures Postrust against PostgREST, Hasura and PostGraphile. Every server runs as a container on one docker network against the same PostgreSQL instance and the same dataset, memory is read the same way for all of them, and each keeps its own default pool and worker settings. Tuning one and not the others measures the tuning rather than the tool.

Terminal
./scripts/bench-compare.sh

# Alpine base images instead of Debian
VARIANT=alpine ./scripts/bench-compare.sh

# One tool at a time
ONLY=postrust,postgrest ./scripts/bench-compare.sh

Two matrices come out of it, because Hasura and PostGraphile expose no REST surface and PostgREST exposes no GraphQL. Results are written as results.json, and the figures published on the comparison pages are copied from that file rather than retyped.

REST, requests per second

ScenarioPostrustPostgREST
point lookup14,4794,323
25-row page13,8004,152
filtered + ordered page11,2927,392
range filter on numeric10,8927,851
25-row page + embed9,4886,005

GraphQL, requests per second

ScenarioPostrustHasuraPostGraphile
single row by primary key12,4404,53510,231
25-row page8,4774,9076,593
25-row page + embed5,1933,5883,495

PostgREST appears only in the REST table and the two GraphQL engines only in the GraphQL one, because neither exposes the other surface. 3000 requests at concurrency 50, median of 5 runs after 500 warm-up requests, on Darwin 25.2.0 arm64 against postgres:16. Figures from one run are comparable with each other; figures from different runs on a busy machine are not, which is why comparing two versions of the same code means running both side by side rather than one after the other.

Per-tool numbers, including the scenarios where another tool is faster, are on the comparison pages.

Binary size

Size depends on the feature set, so quoting one number without the other is misleading. The published Docker image and release binaries are built with admin-ui, so ~5 MB describes what you actually download.

BuildBytesSizeContents
Default features3,070,0802.93 MiBREST + GraphQL only
With admin-ui5,220,8644.98 MiBWhat the Docker image and releases ship

Measured on macOS arm64, LTO release build, stripped. A Linux x86_64 build differs somewhat.

Throughput and latency

Against a 100,000-row table with indexes on category and price, 3,000 requests per scenario at concurrency 50.

Scenarioreq/sp50p95p99
Point lookup
?id=eq.42
60658.0 ms10.0 ms22.0 ms
25-row page
?select=id,name,price&limit=25
60658.0 ms11.0 ms13.0 ms
Filtered + ordered page
?category=eq.cat-5&order=id.desc&limit=25
50259.0 ms14.0 ms23.0 ms
Page with exact count
Prefer: count=exact
63737.0 ms11.0 ms19.0 ms
Range filter on numeric
?price=gt.50&select=id,price&limit=25
64687.0 ms10.0 ms14.0 ms

Apple M-series laptop, PostgreSQL 18 in Docker, ab as the load generator, all over loopback.

Memory

10.0 MB
RSS before serving any request
13.3 MB
RSS after 15,000 requests

Sustained load at concurrency 50 adds a few megabytes over idle and then levels off. Requests that return very large result sets are the exception: fetching all 100,000 rows in one response pushes RSS an order of magnitude higher, and the allocator does not immediately return it. Paginate.

Caveats

Read these before quoting any throughput number, including ours.

Everything shares one machine

PostgreSQL, Postrust and the load generator compete for the same cores over loopback. These numbers compare Postrust against itself across changes; they are not capacity planning figures.

Latency is dominated by contention

At concurrency 50 on a laptop, p50 sits around 8 ms. The same request against an idle server is roughly an order of magnitude faster.

ab is the weakest load generator

It is HTTP/1.0 only and its percentiles are coarse. Install oha for numbers worth comparing between runs.

Cold start is measured elsewhere

The ~50 ms Lambda cold-start figure comes from a different environment and is not produced by this harness.

PostgREST is not measured here

Figures for other projects in comparison tables come from their own documentation, not from this script.

Correctness first

A benchmark that measures a broken endpoint is worse than no benchmark, so the harness verifies every scenario returns a success status before timing it, and the query-parameter behaviour it depends on is covered by integration tests against a real database.

DATABASE_URL="postgres://postgres:postgres@localhost:5432/postrust_test" \
  cargo test -p postrust-server --test query_params -- --ignored