Benchmarks

Serving capacity

How many requests one StateSync-GKR server proves and returns within one second with version 1.2.0, measured against v1.1.0 on the same hosts on the same day, with tail latency, long runs, overload, the one-connection network limit and compute cost per million requests.

This page reports what one server sustains with version 1.2.0 when every request travels the whole path. A request is offered on a fixed schedule, proved by the reference server from Serving components, sent as a complete encoded proof over TCP to a separate frontend and accepted there against its original request. Each version comparison pairs v1.2.0 with v1.1.0 measured on the same host type on the same day. The results were measured on the released version.

Results at a glance#

Resultv1.2.0v1.1.0, same dayConditions
Capacity within 1 s, 192 cores3,300 requests/s over one connection; at least 5,000 over two1,150 requests/s, the highest rate it was offeredContinuous mode, 48-core frontend; the two-connection value from 15-second screens
Capacity within 1 s, 96 cores3,000 requests/s500 requests/s; 1,000 failedContinuous mode, 15-second runs
Capacity within 1 s, 48 cores2,000 requests/s500 requests/s; 1,000 failedContinuous mode, 15-second runs
p99 at 1,000 requests/s, 192 cores33.7–33.9 ms433.8–475.1 msThree 60-second runs, 16-core frontend
Long run, 192 cores5,940,000 requests in 30 minutes at 3,300/s, every one within 1 sOne connection
Overload, 192 coresPeak server memory 535.2 MiB sampled (393.5 MiB by GNU time) at 5,100 requests/s under a 2,760 MiB limit; normal exit60 seconds
Compute cost per million requests, 192 coresAbout $0.87 at 3,300/sAbout $2.50 at 1,150/s, an upper boundServer VM only, estimate
Serving capacity

More requests within one second on every machine size.

3,300/swithin 1 s on one 192-core server, v1.2.0
01,0002,0003,0004,0005,0005002,00048 coreslimit: server CPU5003,00096 coreslimit: one connection1,1503,300≥ 5,000192 coreslimit: one connection

Highest offered rate at which every repeat accepted every timed request within 1 s: a 1:1:1 mix of membership, non-membership and update at depth 24, a separate frontend that verifies every proof, and v1.1.0 measured on the same day as v1.2.0. On 192 cores the one-connection v1.2.0 value is confirmed by three 60-second runs and the two-connection value comes from 15-second screens; the 48- and 96-core values come from one 15-second run per rate. On 48 cores the limit was server CPU; on 96 and 192 cores it was the reply traffic of one connection while the server had CPU to spare. Two connections carried 5,000 per second with the frontend near its own limit, so that value is a lower bound. v1.1.0 was tried at fewer rates, so its limit lies between the value shown and the next rate tried. Past their limits, in runs whose frontend was not the limit, v1.2.0 completed 3.31 times (48 cores) and 4.10 times (96 cores) as many requests per second as v1.1.0.

Capacity within one second means the highest offered rate at which every repeat accepted every timed request within 1 s, judged only on runs where the frontend was not the limit. For v1.1.0 the same-day grid was coarse, so its limit lies between the value shown and the next rate tried; the earlier campaign failed at 650 requests/s on 48 cores and at 1,150 on 192 cores.

Capacity within one second#

192 cores and the network limit#

Each reply carries one complete encoded proof: 176,948, 181,646 and 186,992 bytes for membership, non-membership and update at depth 24. At 3,300 replies per second that is 4.82 Gbit/s from the server to the frontend.

Network limit

One connection tops out near 5 Gbit/s. Two connections carry more.

4.96 Gbit/sreply traffic where one connection levelled
01,0002,0003,0004,0005,0001,0002,0003,0004,0005,000Offered requests per secondCompleted requests per second4.96 Gbit/s, one connection7.30 Gbit/s, two

Median of the processes at each offered rate on one 192-core VM with a separate 48-core frontend; traffic counts reply frames, each carrying one complete encoded proof of 176,948 to 186,992 bytes. Over one connection the server's send rate followed the offered load up to 3,300 per second and stopped near 4.96 Gbit/s; in every run up to 3,400 per second, server CPU stayed at or below 50% and frontend CPU at or below 52% over the run. In 15-second screens with two connections to the same server, every request up to 5,000 per second was accepted within 1 s at 7.30 Gbit/s.

Over one connection, the highest rate with every request within one second was 3,300 requests/s, three 60-second runs in each v1.2.0 mode, with p99 35.4–38.5 ms in continuous mode and 225.1–300.3 ms batch-fused. At 3,400 requests/s the server's send rate levelled at about 4.96 Gbit/s; in every run up to that rate, server CPU stayed at or below 50% and frontend CPU at or below 52% over the run. In 15-second screens with two connections to the same server, every request up to 5,000 per second was accepted within one second at 7.30 Gbit/s. The one-connection limit therefore came from the network path of a single connection between the two VMs, not from proving. A deployment that needs more from one server opens more connections, as the two-connection runs did.

At 5,000 requests/s over two connections, frontend CPU reached 74%, the level at which these studies mark the frontend as a possible limit, so 5,000 is a lower bound for the server. Past the one-connection limit, continuous runs completed about 2,000 to 2,200 requests per second and batch-fused runs about 3,270 to 3,290, both below the offered rate.

48 and 96 cores#

Hostv1.2.0 within 1 sFirst failing rateWhat set the limitHighest completion rate measured
48 cores2,000 requests/s, p99 147.0 ms2,400: 30.0% within 1 s, p99 3.20 sServer CPU at 96%1,983/s against 599/s for v1.1.0, 3.31×
96 cores3,000 requests/s, p99 37.6 ms4,000: 35.2% within 1 s, p99 2.42 sReplies at 4.95 Gbit/s on one connection, with the return budget full and server CPU at 83%3,386/s against 825/s for v1.1.0, 4.10×

On 48 cores, server CPU reached 96% at the limit, so proving set it. On 96 cores, the server still had CPU to spare at the limit: its return budget held all 192 permitted replies, workers waited 259 seconds in total for a permit, and the reply traffic matched the one-connection limit seen on 192 cores. On the same day, v1.1.0 met one second at 500 requests/s on both machines and missed it at 1,000, with 7.9% (48 cores) and 21.1% (96 cores) of requests within one second.

With the 16-core frontend, the 48-core server met one second at 1,600 requests/s with p99 34.7 ms. At 1,800 and 2,000 it still did, but the frontend ran at 73% and 81% CPU, so those runs were judged on the 48-core frontend instead. The highest completion rates in the table likewise come from runs whose frontend was not the limit.

Two 48-core servers or one 96-core server#

Deployment

With v1.2.0, both ways of splitting the cores hold the same tail.

33.7–34.1 msp99 at 1,000/s in both splits of 96 cores
0 ms500 ms1 s1.5 s2 s2.5 s3 s284282330343434Two 48-core workers2.75 s2.55 s2.74 s343434One 96-core worker

Three 20-second runs per policy at 1,000 requests per second with the same total worker resources (96 cores, 192 GiB) and a separate 16-core frontend; bar labels are in milliseconds unless marked. With v1.1.0, two workers with batch cap 48 each accepted every request within 500 ms and one worker with batch cap 96 built a queue. With v1.2.0 in continuous mode both policies accepted every request within 1 s, using 20.8 and 20.5 worker cores. This compares deployment policies, not topology alone.

At 500 and 1,000 requests/s with the same total worker resources and the 16-core frontend, v1.2.0 accepted every request within one second in all twelve runs, with p99 between 33.3 and 34.1 ms in both deployments. With v1.1.0, one 96-core worker failed at 1,000 requests/s. With v1.2.0, the choice between one larger server and two smaller ones no longer decides the tail at these rates.

Tail latency at a fixed load#

Tail latency

The same 1,000 requests a second, with a tail under 34 ms.

33.7–33.9 msp99 at 1,000/s on 192 cores, v1.2.0
0 ms100 ms200 ms300 ms400 ms500 ms600 ms0102030405060Second of the input window99th percentile, per arrival second

Three 60-second runs per series on one 192-core VM with a separate 16-core frontend; the lines show run 1. Every run accepted all 60,000 timed requests within 1 s. Process p99: v1.1.0 433.8–475.1 ms on the v1.2.0 measurement day (426.0–443.5 ms in the earlier campaign), v1.2.0 batch-fused 95.6–96.0 ms and v1.2.0 continuous 33.7–33.9 ms. Over the input window v1.2.0 continuous kept 20.6 of the 192 cores busy on average, against 65.1 for v1.1.0.

Offered rate, 192 coresv1.1.0, same dayv1.2.0 batch-fusedv1.2.0 continuous
100 requests/s, three 20-second runsp99 233.0–233.3 msp99 33.1–33.4 ms
250 requests/s, three 20-second runsp99 248.1–248.9 msp99 33.3 ms
1,000 requests/s, three 60-second runsp50 265.3–266.4 ms, p99 433.8–475.1 msp50 61.2–61.4 ms, p99 95.6–96.0 msp50 25.6–25.7 ms, p99 33.7–33.9 ms
1,150 requests/s, 60 secondsp99 580.1 ms, one runp99 96.1–96.3 ms, three runsp99 33.6 ms, three runs

Every run in this table accepted every timed request within one second. In continuous mode the p99 stays at 33 to 34 ms from 100 to 1,150 requests per second: a request no longer waits for a batch to fill or for the slowest proof of a batch. At 1,150 requests/s the median job took 18.7 ms from dispatch to finish on its worker.

The earlier v1.1.0 campaign met one second for only 70.37% of requests at 1,150 requests/s, while v1.1.0 on the v1.2.0 measurement day met it for all of them. Both are shown in the next figure.

Peak delivery

At 1,150 requests a second, replies stay in the tens of milliseconds.

33.6 msp99 at 1,150/s on 192 cores, v1.2.0
0 ms500 ms1 s1.5 s2 s0102030405060Second of the input window99th percentile, per arrival second2 s budget1 s

One 60-second window per line at 1,150 requests per second on a 192-core server with a separate 16-core frontend. In the earlier v1.1.0 campaign (192 workers, batch cap 192) all 69,000 requests were accepted within 2 seconds and 70.37% within 1 second while the queue grew; on the v1.2.0 measurement day v1.1.0 met 1 second for every request with a p99 of 580.1 ms. Version 1.2.0 met 1 second in three runs per mode, with p99 96.1–96.3 ms batch-fused and 33.6 ms continuous.

Where the gain comes from#

The serving components can run the v1.1.0 prover code, and the v1.2.0 binary can run the earlier serving loop, so the two changes were also measured apart. At 1,000 requests/s on 192 cores, three 60-second runs each:

ProverServingp99
v1.1.0v1.1.0 server433.8–475.1 ms
v1.1.0 prover codev1.2.0 batch-fused233.6–234.8 ms
v1.1.0 prover codev1.2.0 continuous74.9–75.3 ms
v1.2.0original, the earlier loop159.2–162.0 ms
v1.2.0v1.2.0 batch-fused95.6–96.0 ms
v1.2.0v1.2.0 continuous33.7–33.9 ms

Either change lowers the tail by itself, and together they lower it most. One cause in the earlier loop was measured directly. After the last reply of a full batch, the loop paused for a median 45.50 ms before it could start the next batch; with release timers added at 1,150 requests/s, freeing the batch's intermediate data on one thread took 45.27 ms of that pause. In both v1.2.0 modes, the worker that computed a job also frees it, in a median of 22.5 to 23.8 µs per job. At 1,000 requests/s v1.2.0 continuous kept 20.6 of the 192 cores busy on average, against 65.1 for v1.1.0.

Long runs and a second host#

Long run

Thirty minutes at 3,300 requests a second, with a steady tail.

5,940,000requests in 30 minutes, every one within 1 s
0 ms10 ms20 ms30 ms40 ms50 ms60 ms051015202530Minute of the run99th percentile, per second

One 1,800-second run in continuous mode (190 jobs in flight) on one 192-core VM with a separate 48-core frontend over one connection. Whole-run p50 / p99 / maximum: 26.7 / 35.3 / 58.8 ms. Straight-line fits over the run: pending requests −0.006 per minute, per-second p99 −0.006 ms per minute and server memory +0.08 MiB per minute. A second 192-core VM accepted every request within 1 s in three 60-second runs at the same rate.

The 30-minute run at 3,300 requests/s accepted all 5,940,000 timed requests within one second, with p50 26.7 ms, p99 35.3 ms and a maximum of 58.8 ms. Straight-line fits over the run show the queue and the tail flat and memory almost flat: pending requests −0.006 per minute, per-second p99 −0.006 ms per minute and server memory +0.08 MiB per minute.

A second 192-core VM repeated the confirmation: three 60-second runs at 3,300 requests/s met one second for every request, with p99 37.6–46.4 ms, and 1,000 requests/s gave p99 34.1 ms. v1.1.0 on that VM had p99 666.8 ms at 1,000 requests/s from the same 48-core frontend.

On 48 cores, the 15-minute run of the earlier campaign was repeated at 500 requests/s:

Sustained serving

Fifteen minutes at a steady 500 requests a second.

33.4 msp99 for 15 minutes at 500/s on 48 cores, v1.2.0
0 ms100 ms200 ms300 ms400 ms500 ms03691215Minute of the run99th percentile, per second500 ms budget

One 900-second run per version on a 48-core server: a 1:1:1 mix of membership, non-membership and update at depth 24, offered at 500 per second, with a separate 16-core frontend. Latency ends when the complete proof is cryptographically accepted against its original request. Both runs accepted all 450,000 requests within 500 ms. Whole-run p50 / p99 / maximum: v1.1.0 (48 workers, batch cap 48) 165 / 267 / 307 ms; v1.2.0 continuous (46 jobs in flight) 25.6 / 33.4 / 34.0 ms.

Metric, 900 seconds at 500 requests/sv1.1.0, earlier campaignv1.2.0 continuous
Accepted within 1 sAll 450,000All 450,000; none late or rejected
p50 / p99 / p99.9 / maximum165 / 267 / 275 / 307 ms25.6 / 33.4 / 33.6 / 34.0 ms
Largest server queue841
Peak server memory133 MiB117 MiB
Mean server CPU25.5 cores9.6 cores

Overload: bounded memory#

Overload

Above capacity, server memory stays bounded.

535.2 MiBsampled peak server memory at 5,100/s
0 MiB500 MiB1,000 MiB1,500 MiB2,000 MiB2,500 MiB3,000 MiB335.6 MiBRun 13,300/s337.3 MiBRun 23,300/s338.0 MiBRun 33,300/s535.2 MiBMemory-limited5,100/s493.4 MiBSend-limited3,400/s

Largest resident memory of the server process sampled from /proc during each run, in continuous mode with the default return budget of 192 responses and 64 MiB. GNU time reported lower peaks for the same processes: 393.5 MiB in the memory-limited run, 372.8 MiB in the send-limited run and 218.5 to 233.4 MiB at 3,300 per second. The memory-limited run offered 5,100 requests per second for 60 s; the send-limited run offered 3,400 per second with the server's outbound traffic shaped to 400 Mbit/s. In both, the frontend's own limit of 8,192 pending requests held back the excess before sending, the server rejected no request and both processes exited normally. The three runs at the confirmed rate of 3,300 per second are shown for scale.

Two runs drove the 192-core server past its capacity on purpose:

RunTimed requestsAcceptedWithin 1 sHeld back by the frontendServer rejectionsPeak server memory, sampled (GNU time)
5,100 requests/s for 60 s, server memory limited to 2,760 MiB306,000125,769 (41.1%)1.15%180,2310535.2 MiB (393.5 MiB)
3,400 requests/s for 60 s, server send rate shaped to 400 Mbit/s204,00020,131 (9.9%)50 requests179,7560493.4 MiB (372.8 MiB)

Peak server memory is the largest resident memory of the server process sampled from /proc during the run; GNU time reported a lower peak for the same process, shown in brackets. In both runs the server's memory stayed far below the limit, about a fifth of it in the memory-limited run, and both processes exited normally. Finished replies wait under the return budget: when the frontend cannot take replies fast enough, workers wait for a permit instead of growing a queue of replies, as the full budget in the 96-core runs above showed. The frontend reached its own limit of 8,192 pending requests before the server's admission cap filled, so the excess was held back on the frontend and the server rejected nothing; in the send-limited run, 4,113 requests had no reply when the 15-second drain ended. These requests count as failures in the success fractions above.

When the admission cap does fill, the server refuses each further request with server_total_queue_full instead of queueing it; Serving components lists every refusal and the checks that exercise them. In the earlier v1.1.0 campaign at 2,000 requests/s with the 16-core frontend, 14,105 requests met that frontend limit and were never sent; with v1.2.0 none were.

Compute cost per million requests#

Compute cost

Each request costs less to serve.

$0.87per million requests at 3,300/s, estimate
$0$1$2$3$2.87$3.11v1.1.0, earlier campaign1,000/s$2.50$2.71v1.1.0, same day1,150/s$0.87$1.09v1.2.0 continuous3,300/s

Estimate: the on-demand hourly price of each VM type, checked on 25 October 2026, divided by the requests served in an hour at the rate shown, which is the highest rate with every request within 1 s, assuming continuous operation. The frontend column adds the separate verifying frontend (16 cores for v1.1.0, 48 cores for v1.2.0). Storage, network transfer, idle time and tax are excluded. On the same day v1.1.0 was not offered more than 1,150 requests per second, so its cost is an upper bound.

Deployment, 192 coresRateServer VMServer and frontend VMs
v1.2.0 continuous3,300 requests/sAbout $0.87About $1.09 (48-core frontend)
v1.1.0, same day1,150 requests/sAbout $2.50About $2.71 (16-core frontend)
v1.1.0, earlier campaign1,000 requests/sAbout $2.87About $3.11 (16-core frontend)

The estimate divides the on-demand hourly price of each VM type, checked on 25 October 2026 ($10.35 an hour for the 192-core VM), by the requests served in an hour at the highest rate with every request within one second, and assumes continuous operation at that rate. Storage, network transfer, idle time and tax are excluded, and it is not an invoice. On the same day v1.1.0 was not offered more than 1,150 requests/s, so its cost is an upper bound.

Conditions#

ItemCondition
Softwarev1.2.0, release tag v1.2.0, commit 8b2908160f36adc233d7b869a403d2ee4d3502a5; v1.1.0 from the binaries archived by the earlier campaign
v1.2.0 serverThe reference server in continuous mode unless a row says otherwise: --workers equal to the cores, the default --max-in-flight (46, 94 or 190), --max-queue 8,192 and the default return budget of 192 replies and 64 MiB. Batch-fused runs used --batch-max equal to the cores and a 5 ms maximum wait
v1.1.0 serverThe earlier campaign's serving caller: one compute batch at a time, batch cap equal to the cores, 5 ms maximum wait
HostsC8a VMs with 48, 96 and 192 cores and 96, 192 and 384 GiB, running Amazon Linux 2023 with kernel 6.18. The earlier campaign used Ubuntu 22.04 with kernel 6.8; pairing each version comparison on one day keeps that difference out of it
FrontendThe earlier campaign's load frontend on a separate 16-core VM, or on a 48-core VM with 44 verifying workers where the 16-core frontend would have been the limit. It verifies every reply with the strict encoded verifier against the original request and holds at most 8,192 pending requests
WorkloadMembership, non-membership and update in a 1:1:1 mix at depth 24; 192 fixed fixtures per kind, cycled; periodic arrivals scheduled independently of replies
LatencyFrom the scheduled arrival to acceptance at the frontend, on the frontend's monotonic clock; nearest-rank quantiles
ConnectionsOne TCP connection unless a row says two; the two-connection runs named the server's address twice
RunsOn 192 cores, three 60-second runs at each judged v1.2.0 rate after 15-second screens (v1.1.0 on the same day: three at 1,000 requests/s, one at 1,150), 15-second runs for two connections from 3,400 to 5,000 requests/s, one 30-minute run and overload runs of 60 seconds; on 48 and 96 cores, one 15-second run per offered rate
Frontend limitA run counts as frontend-limited when frontend CPU is above 70% of its cores, or its verification queue p99 is above 50 ms, while the server stays below 90% CPU. Such runs are left out of the capacity judgment

On the v1.2.0 measurement day, v1.1.0 completed proofs within 1.1% of its earlier campaign's compute rate on 192 cores, so the earlier campaign's v1.1.0 series stays comparable on the same axes. Changing the frontend from 16 to 48 cores left v1.2.0 at p99 33.9 ms at 1,000 requests/s and did not lower v1.1.0's tail (458–649 ms), so the tail differences come from the server. Benchmark methodology describes the statistics and outcome classes.

Earlier v1.1.0 serving results#

The serving results of the earlier v1.1.0 campaign, including its 48-core fine-tuning grid, are kept as their own series in Benchmark results. They are never divided by a later run.

Next steps#