Deploy · Benchmarks

What each shape serves

Every shape on the Deploy page runs the same server. What changes is what you add around it, and so what it can serve. These are the numbers measured for each combination, with the release, the build and the report behind them. Where a combination hasn't been measured yet, it says so.

01 · At a glance

Every combination, measured

One row per combination, in the order of the Deploy page's shapes and then the add-ons. Click a name for its block below.

As the image ships it · 0.47 · 2026-10-3016,403 calls/sFrom one pod: the image's data plane held to 2 CPUs, one Python worker, a PostgreSQL store with the metadata cache, 64 callers, for 10 minutes. 9,842,067 runs, every one recorded; p99 5.5 ms.The image measurement →
Data plane against Python · MySQL · 2026-10-277.9× per coreEach at its best on the same 8 cores, 64 callers, both with the metadata cache: 23,619 calls/s through the data plane with 2 Python workers behind it (p99 3.8 ms), against 2,976 from 8 Python workers (p99 93 ms).Performance v5 →
CombinationRelease (date)BuildCalls/sLatencyGateway coresRuns recordedReport
One worker, PostgreSQL store0.43 (2026-10-26)source852 (827-892)PK lookup, 32 callers1.27 ms p50, one caller1 worker—Perf v4 →
+ metadata cache0.43 (2026-10-26)source1,012 (991-1,043)1.00 ms p50; 1.60 ms with the database 0.5 ms away1 worker—Perf v4 →
+ masking and a table allow-list0.43 (2026-10-26)source809964 with the cache—1 worker—Perf v4 →
Several workers0.42 (2026-10-24)source2,727 at 8 workerscached, 64 callers—8 workers on 4 physical cores—Perf v3 →
MySQL, six databases0.43 (2026-10-26)source662-875 per worker+1.0-1.3 ms over MySQL itself1 worker—MySQL v2 →
+ Rust data plane Exp0.44 prototype (2026-10-27)source20,000 fixed, PostgreSQL, cached64 connections; 6,734 per gateway core (7.3×)0.11 ms p50, one caller, cached; p99 1.8 ms at 20,000/s2.97Every runPerf v5 →
+ data plane, one 2-CPU pod Exp0.47 (2026-10-30)image16,403 for 10 minutesp99 5.5 ms1.92 data plane + 0.50 Python9,842,067 (100%)Image run →
+ ClickHouse run store Exp0.45.0 (2026-10-28)source20,000 for 30 minutes35,501 flat outp50 0.55 ms, p99 2.80 ms3.02 at 20,000/s35,999,897 of 35,999,897Run store →
Calls a second per gateway coreA primary-key lookup, 32 callers flat out, one session (Performance v5, 2026-10-27). * At a fixed 20,000 calls/s over 64 connections. Log scale.
Python servesThe data plane serves
5001,0002,0005,00010,000PostgreSQLPython: 747 calls a second per gateway corePython747Python + metadata cache: 925 calls a second per gateway corePython + metadata cache925Data plane: 2,361 calls a second per gateway coreData plane2,361Data plane + cache, at 20,000/s *: 6,734 calls a second per gateway coreData plane + cache, at 20,000/s *6,734MySQLPython: 616 calls a second per gateway corePython616Python + metadata cache: 769 calls a second per gateway corePython + metadata cache769Data plane: 2,144 calls a second per gateway coreData plane2,144Data plane + metadata cache: 3,253 calls a second per gateway coreData plane + metadata cache3,253

All eight from one session and one build, so they compare. The image's data plane serves at 0.95-0.99× of this build. Performance v5 →

02 · By shape

What you get, one combination at a time

What you turn on, what it served, what to plan around, and what the measurement doesn't cover.

01

Single server: one worker on a PostgreSQL store

0.43 · 2026-10-26source
Turn on
The image as shipped: one gunicorn worker, 8 threads, QUERYAPIGATE_DATABASE_URL=postgres://…
852 calls/sA primary-key lookup on 10 million rows, 32 callers.
1.27 msOne call on its own, p50. PostgreSQL itself answers in 0.03 ms.
4-5 ms p99Up to 75% of capacity. About 16 ms at 90%; 108 ms at 100%, where a queue builds.
Plan around

Size at 75% of capacity. One worker handles about 670 fast calls a second on CPU, 780 with the metadata cache.

Caveats
  • The SQLite store hasn't been measured under load since 0.43; the September SQLite report covers the example APIs.
02

+ metadata cache

0.43 · 2026-10-26source
Turn on
QUERYAPIGATE_METADATA_CACHE_TTL
1,012 calls/s+19% over 852; 1.00 ms p50.
1.60 msWith PostgreSQL 0.5 ms away. 4.39 ms on 0.43 without the cache; 6.58 ms on 0.42.
7 → 4 → 1Round trips a call: 0.42 made 7, 0.43 makes 4, 0.43 with the cache makes 1.
  • On 0.42, at 8 workers, the cache cut the PostgreSQL store's work from 1.65 cores to 0.92 (Performance v3, 2026-10-24).
03

+ governance: column masking and a table allow-list

0.43 · 2026-10-26source
809 calls/sAgainst 852 for a plain key.
964 calls/sWith the metadata cache, against 1,012.
~5%What masking and an allow-list cost.
  • In 0.41 the same governed key served 171 calls a second.
04

Scaled out: several workers or instances

0.42 · 2026-10-24 · Performance v3; MySQL on 0.43source

PostgreSQL, 64 callers, calls a second

WorkersWithout the cacheWith the cache
1734869
21,1961,441
41,7162,112
82,2152,727
  • Measured on 0.42; Performance v4 found 0.43 scales the same way.
  • Eight workers shared four physical cores, so growth is less than linear. A hyperthread is worth about 0.6 of a core: run one worker per physical core.
  • MySQL on 0.43: the laptop's ceiling was 3,500 Order-Status and 4,400 single-row lookups a second, from 12 workers on 6 performance cores.
  • Threads: for fast queries, 2 threads beat the default 8 by 13% (1,012 against 896 calls/s, 0.43). Keep 8 or more for slow queries, exports and /events. Set with QUERYAPIGATE_THREADS.
Caveats
  • These are workers on one machine. Several instances on a shared PostgreSQL store with Redis haven't been load-tested; see Not measured yet.
05

MySQL: one server, six microservice databases

0.43 · 2026-10-26source

Calls a second, one worker. MySQL directly: 32 threads, from MySQL v1 (2026-10-24)

Query (databases)MySQL directly0.420.430.43 + cache
Customer by id (crm)75,420647743875
Order-Status (crm, orders)25,063573662727
Stock-Level (org, orders, inventory)5,343630751833
  • +1.0-1.3 ms a call. MySQL itself answers these in 0.03-0.42 ms, and joining across databases costs it no more than within one (MySQL v1, 2026-10-24).
  • +52% over 0.42 with MySQL 0.5 ms away (680 against 446 calls/s); one caller, p50 2.89 ms.
  • Turning off gunicorn's access log and the request lines gave +21%: QUERYAPIGATE_ACCESS_LOG=off, QUERYAPIGATE_LOG_REQUESTS=off.
  • 9.5 million requests, every one returned 200.
06

+ Rust data plane

0.44 prototype · 2026-10-27source Exp
Turn on
From 0.47.0: dataplane.enabled: true in Helm, or deploy/scale-out/docker-compose.dataplane.yml; guide 64
7.9× per coreMySQL, both at their best on the same 8 cores, 64 callers: 23,619 against 2,976 calls/s; p99 3.8 against 93 ms.
0.11 msOne call on its own through the data plane with the metadata cache, p50 (0.20 ms without it).
0Response and history differences from Python over 13,500 calls in the differential gate.

A lookup, 32 callers flat out (Performance v5). * Data plane + cache on PostgreSQL: a fixed 20,000 calls/s over 64 connections, every run recorded.

PythonPython + cacheData planeData plane + cache
PostgreSQL, calls/s8731,04312,68920,000* at 2.97 cores
per gateway core7479252,361 (3.2×)6,734* (7.3×)
p9942.8 ms35.9 ms5.1 ms1.8 ms*
MySQL customer lookup, calls/s79489815,87123,961
per gateway core6167692,144 (3.5×)3,253 (4.2×)
p9947.0 ms39.7 ms3.2 ms2.1 ms
  • Python decides and Rust serves. Python computes a plan on the first call; the data plane serves the calls after it while the key, query and connection rows still match; Python records every run.
Plan around

One Python process records about 17,000 runs a second. Above that, add a second Python worker or the run store.

Caveats
  • Flat out at 32 callers, the data plane + cache on PostgreSQL served 27,519 calls/s, 3,904 per core, p99 2.1 ms, but kept only 51-100% of runs with one Python worker behind it.
  • Across lookups, 3.2-6.5× per core without the cache and 4.0-9.4× with it; the top ends are one caller at a time. Paging on PostgreSQL: 2.8× without the cache, 3.5× with it.
  • Governed Order-Status on MySQL: 2,252 against 13,863 calls/s (p99 116 against 6.4 ms).
  • Calls it forwards, such as writes, pay +0.35 ms at p50 for the extra hop. The data plane uses about 19 MB of memory; a gunicorn worker 52-64 MB.
  • Where it doesn't help: writes (0.8× per core), the first call for each query and key, administrators, signed-in users, other formats, streaming and rate-limited keys. Python serves all of these.
07

+ data plane, as the image ships it

0.47 · 2026-10-30image Exp
Turn on
One pod: the image's data plane held to 2 CPUs, one Python worker, a PostgreSQL store with the metadata cache, no run store, 64 callers. dataplane.enabled: true
16,403 calls/sFor 10 minutes: 9,842,067 runs, 100% recorded, p99 5.5 ms.
16,788-17,018Calls a second over 5 rounds of 30 s; 100% recorded, p99 5.5-5.7 ms.
1.92 + 0.50CPUs used: the data plane, then Python.
~8,900Calls a second per data-plane CPU in the 30-second rounds (about 8,540 over 10 minutes), more than the 7,000 ADR 0030 assumed.

The image against the from-source build, calls a second per gateway core

From sourceImage
Plain lookup4,0954,058
Masked, table-restricted lookup3,9163,908
100-row page2,8292,691
Plan around

This is the most deployable number on the page: the shipped image, in one pod, recording every run. The image's data plane serves at 0.95-0.99× of the from-source build.

08

+ ClickHouse run store

0.45.0 · 2026-10-28; 250 million runs on 0.46.0 · 2026-10-29source Exp
Turn on
QUERYAPIGATE_RUN_STORE_URL; ClickHouse 24.8 or later, over https unless it's on the same machine
35,999,897Of 35,999,897 runs recorded in a 30-minute soak on 0.45.0; 0 late events, insert lag p99 13 ms.
100% at 35,501/sPlain lookups flat out, 64 callers. The SQL store recorded 52% at 34,240/s.
13.8 bytesA run in events at 250 million runs (13.7 in history): 6.9 GB from 134 GB of raw entries. 13.0 and 13.4 at 72 million.
0Events missing or repeated over 7,515 reconnects and 35,084,849 events.

At 20,000 calls a second

SQL storeRun store
Runs recorded100%100%
p50 / p990.62 / 2.40 ms0.55 / 2.80 ms
Gateway cores3.403.02 (−11%)
of which Python0.490.13

Reads at 250 million runs, p95 (ADR 0029's bar)

Readp95Bar
Newest 100 in history12.5 ms100 ms
One key's newest 10073.6 ms100 ms
The Console's versions and run counts37.3 ms100 ms
A 24-hour usage report66.7 ms500 ms
One key's full replay, 34,893 events6.2 s10 s
  • Governed lookups flat out: 33,530/s with 100% recorded, against 29,543/s with 54% on the SQL store.
  • About 22 GB a day in events at 20,000 runs a second, and as much again for each day of history kept at sample rate 1.
Caveats
  • One laptop and a single ClickHouse server; replicated clusters are untested.
  • The 250 million runs are a 30-minute soak's real runs, copied through the same views.
  • One queryapigate events process delivers about 6,500-10,000 events a second per stream.
  • Measured with the data plane built from source: recording, cores and the soak on the branch that shipped as 0.45.0; storage, reads and the replay on main as shipped in 0.46.0. The run store with the shipped image hasn't been measured.
03 · What features cost

The price of each switch

Each with its release and source. Older measurements predate 0.43 and are tagged.

Masking and a table allow-list
~5%0.43, Performance v4
Metadata cache
+19% calls/s; 4 round trips a call become 10.43, Performance v4
Access log and request lines off
+21%0.43, MySQL v2
2 threads instead of 8, fast queries
+13%0.43, Performance v4
Where the CPU goes
store 22.9%, logging 21.2%, gunicorn, sockets and HTTP 18.7%, Flask 10.8%, psycopg2 and the pool 7.6%, request handling 5.6%, other 13.2%0.43, no cache, Performance v4
Response cacheOlder measurement
A hit 5.8 ms median; a miss, which also writes the cache, 14.2 ms; no cache at all 6.8 ms0.10.0, MySQL, 2026-09-30
Streaming 1 million rowsOlder measurement
About 0 MB extra memory, against 337-988 MB buffered; slower to finish: 13.0 against 3.3 s for narrow rows0.3.0, MySQL, 2026-09-24
Tracing at 100% samplingOlder measurement
~1 ms a request; nothing measurable at 10%0.23.0 release notes, 2026-10-11
Quotas across two instances sharing RedisOlder measurement
1,016 served against a 1,000-call quota, under load; 1,615 without Redis0.24.0 release notes, 2026-10-11
Saved query against ad-hoc SQLOlder measurement
6.39 against 4.05 ms p50 on a 100-row table (~2 ms). 0.43's +1.0-1.3 ms supersedes it.0.10.0, MySQL, 2026-09-30
04 · Not measured yet

What hasn't been measured

Each of these matters for a production decision, and none has a number yet. Read it as the benchmark roadmap.

Not measured yetSeveral instances under loadOn a shared PostgreSQL store with Redis. High availability is tested for correctness on every push, not for throughput.
Not measured yetKubernetes under sustained loadSeveral pods, the autoscaler and rolling upgrades, under load.
Not measured yetTLS and a proxy in the pathA load balancer or Caddy in front. Every result is loopback or a simulated network on one machine.
Not measured yetThe data plane behind a proxy, at throughputOnly checked for function: three data planes served 26 of 30 calls behind Caddy.
Not measured yetThe MCP server's throughputAgents' calls go through the same core, but the MCP server itself hasn't been load-tested.
Not measured yetThe SQLite store on 0.43 or laterThe September SQLite report predates 0.43.
Not measured yetOther data sources on current releasesClickHouse, DuckDB, H2, Trino and cloud warehouses. The September reports for ClickHouse, DuckDB, H2 and SQLite predate 0.43.
Not measured yetThe run store with the image, or replicatedWith the shipped image's data plane, and with a replicated ClickHouse.
Not measured yetSeparate hosts and long soaksSeparate hosts, long soaks at peak, failure injection, and p99.9 under bursts: deferred since 0.43.
05 · Source and image

Which build each number came from

Source or image matters for Python.

Most rows were measured from source, on the laptop's Python 3.12.3. The Docker image's Python, from python:3.12-slim, links against a shared libpython and is about 30% slower per CPU for every call Python handles (BACKLOG #104). The image's Rust data plane serves at 0.95-0.99× the from-source build. So data-plane rows carry over to the image, and Python-only rows overstate what the container does today.

06 · How it's measured

Measured, not promised

  • One 20-thread laptop on AC power, with the performance profile.
  • The gateway, the database and the load generator each pinned to their own cores: gateway 0-7, database 8-11, load 12-19.
  • Builds take turns in rotating order: 5 rounds per comparison; 3 for the run store cells and the data plane's 64-caller block.
  • A difference counts only when the min-max ranges don't overlap.
  • Open-loop load at fixed rates for latency, corrected for coordinated omission; closed-loop for throughput.
  • A /health check before and after every block.

Absolute numbers depend on the machine; the shape is the portable claim. The full method →

07 · Deep dives

Every number, in full

The ADRs: 0028, the data plane prototype · 0029, the run store · 0030, shipping the data plane