Skip to main content
These results measure the Relay v0.11.7 code under load, on dedicated AWS boxes with a mock upstream. They are the baseline taken before a planned rework of the request engine; the same scenarios will be rerun after it and added as a new tab beside each table. These are measurements of one setup. Your capacity depends on your store, network, request mix and upstream latency; measure your own with relay_overhead_seconds. For how to turn these numbers into a deployment, see Sizing and tuning.

What this measures

Each run puts one relay (or a group of relays behind HAProxy) on its own box, sends it an open-loop request schedule from a generator box, and steps the offered rate up until the relay refuses part of it. The upstream is a mock that answers with a fixed latency model, so every millisecond of variation above it is Relay, the network, or the state store. This is not a service-level objective, and the numbers are not a promise for any other setup. The repository’s in-process benchmarks (make bench-gate, in-memory state, no network) are a regression gate for allocations and are not published as performance figures.

Setup

How to read the numbers

  • Overhead is measured by the generator: the client-side total latency of a request minus the duration the mock upstream reports for it. It includes two network hops inside one availability zone, so it is a little higher than relay_overhead_seconds, which Relay measures inside the process. Each step reports the worse of the two request types.
  • Errors are every non-2xx response, as a share of all responses, the worse of the two request types. A request refused as overloaded (503, or 529 on Anthropic-shaped routes) counts as an error.
  • Latency knee is the highest step at which p99 overhead stays at or under 15 ms with at most 0.1 % errors, with every lower step passing too.
  • Error capacity is the highest served rate at a step with at most 0.1 % errors. Its p99 is always given with it, because it is usually well past the knee.
  • Relay cores and working set cover the last two minutes of each step. Working set is the largest value in that window.
  • A step where the generators fell behind their schedule measures the generator, not Relay. Those steps are marked as excluded and left out of every figure. Where the generators sent less than the step’s rate without falling behind, the table shows what was sent.
  • Usage record delivery was not checked in these runs.

Results by scenario

One relay per box

Served against offered requests per second for 4, 8, 16 and 32 core relaysServed against offered requests per second for 4, 8, 16 and 32 core relays

Served against offered requests per second on one relay, by box size. The v0.11.8 line on 16 and 32 cores is the later run described below, not part of the v0.11.7 tables.

The 32-core ladder steps from 16,000 straight to 32,000 (p99 41.1 ms), so its knee lies somewhere between the two. Its highest served figure comes from a second run on the same boxes; the first run’s 64,000 step was generator-bound.4 cores (c7a.xlarge)8 cores (c7a.2xlarge)16 cores (c7a.4xlarge)Second run on the same boxes:32 cores (c7a.8xlarge)Second run on the same boxes:

Requests per core

Requests per second per core at the latency knee and at error capacityRequests per second per core at the latency knee and at error capacity

Latency knee and error capacity per core. Box cores, 32 for the four-relay group.

Cores are not all busy at error capacity: the 8-core box used 6.87 of its 8 at 24,008 rps, about 0.29 ms of CPU per request for this mix. Per core, the 8-core box carries the most; the 16- and 32-core boxes lose ground to garbage collection and to waiting on the store, which the later v0.11.8 run below partly recovers.

Four relays behind HAProxy

Four 8-core relays (c7a.2xlarge) behind HAProxy 3.2 on a c7a.4xlarge, sharing one store. HAProxy keeps connections to the relays alive (http-reuse always). The generator box was a c7a.8xlarge, smaller than in the single-box runs, and at 64,000 and 80,000 the generators sent slightly less than the step’s rate.
Served against offered requests per second for four 8-core relays behind HAProxyServed against offered requests per second for four 8-core relays behind HAProxy

Four 8-core relays sharing one store, against four times one 8-core box at a quarter of the load.

Latency knee 32,022 rps (p99 1.9 ms); error capacity 62,385 rps (p99 35.8 ms). Relay cores are the sum of the four relays; working set is the largest of the four. Admission p99 is relay_admission_seconds, the time from accepting a request to handing it upstream, which under load is mostly the wait for a store connection.
Four 8-core relays sharing one store carried more than one 32-core relay at the same total core count: 62,385 against 48,005 rps at error capacity. The shared store’s wait limits the group: at the same load per relay, admission p99 in the group was about ten times a lone 8-core box’s (42.8 ms at 16,000 per relay against 4.4 ms). The comparison is not exact, since the group’s generator box was smaller and the later v0.11.8 run reached about 52,000 on 32 cores.

State store

One 16-core relay (c7a.4xlarge), the store on its own c7a.4xlarge. The Valkey run used the v0.11.7 code without its last commit, the default in-flight cap of 2048 and RELAY_UPSTREAM_MAX_IDLE_PER_HOST=1024. Both Dragonfly runs used the v0.11.7 code with the idle cap at its default (the in-flight cap); the default-flags run kept the in-flight cap at 2048, the hash-tag run used 20000. The cap of 2048 does not bind below about 30,000 rps on this box.
Valkey 8, io-threads 4Dragonfly v2.0.2, default flagsDragonfly v2.0.2, —cluster_mode=emulated —lock_on_hashtags
Valkey runs commands on one main thread. It levelled off at about 421,000 commands per second with its main thread near one full core, about 18 commands per request, so the relay plateaued near 23,500 rps. At 16,000 rps the two Lua script calls per request took 83 % of Valkey’s command time. Dragonfly with default flags runs each script as a transaction across shards and levelled off lower; with --cluster_mode=emulated --lock_on_hashtags each script stays on one shard, and 28,048 rps was still under 0.1 % errors at the last step.

Upstream idle connections

One 8-core relay (c7a.2xlarge) on Valkey 8, the v0.11.7 code without its last commit (so the idle cap does not follow the in-flight cap), in-flight cap 2048.
RELAY_UPSTREAM_MAX_IDLE_PER_HOST=128RELAY_UPSTREAM_MAX_IDLE_PER_HOST=1024
With 128 idle connections per upstream host, connections past the cap close after each response and the next requests dial again. Past capacity the relay spent 14.6 % of its CPU dialing and served traffic fell to 14,672 rps at 28,000 offered. With 1,024 it held about 22,000. Error capacity is the same in both runs, about 16,000, because Valkey bounds this box first. Since v0.11.7 the idle cap defaults to the in-flight cap.

Streaming

One 16-core relay (c7a.4xlarge), streaming Anthropic Messages and OpenAI Chat Completions, 8 tokens per event. The mock starts each stream after 300 ms and sends 80 tokens per second; streams last 2.1 s at p50, 15 s at p99 and 3 s on average, so open streams are the start rate times three.
Working set against open streamsWorking set against open streams

Relay working set against open streams on one 16-core relay.

Each open stream costs about 213 KiB of working set and 0.45 cores per 1,000. At 12,000 open streams the p99 overhead rose to 34.9 ms, from scheduler and system-call load across some 52,000 goroutines.

Default in-flight cap

The same boxes at the default RELAY_MAX_INFLIGHT of 2048, generators on a c7a.8xlarge. On 16 and 32 cores the cap, not the box, sets the ceiling near 30,000 rps: requests over it are refused as overloaded while cores sit idle.
8 cores (c7a.2xlarge)16 cores (c7a.4xlarge)32 cores (c7a.8xlarge)

Later run: 16 and 32 cores on the v0.11.8 code

A separate run after v0.11.7, on 16 and 32 cores only, with a development build carrying the Go code of v0.11.8 before its dependency updates. It includes allocation and garbage-collection work on the request path. It compares GOGC=100 with GOGC=400, alternating the two on the same boxes; each row is the mean of two runs, except the 32-core steps from 48,000 up, which ran once. Same setup as the v0.11.7 table otherwise: generators on a c7a.16xlarge, Dragonfly with hash tags on a c7a.8xlarge, RELAY_MAX_INFLIGHT=20000. There are no v0.11.8 figures for 4 cores, 8 cores or the four-relay group. GOGC=400 used about 9 % fewer cores per request on 16 cores and 10–16 % fewer on 32 cores, cut p99 overhead by a quarter to a half, and cost 0.7–1.6 GiB more working set. On 16 cores both settings stop at about 40,000, where the wait for the store binds first.
16 cores (c7a.4xlarge), GOGC 10016 cores (c7a.4xlarge), GOGC 40032 cores (c7a.8xlarge), GOGC 10032 cores (c7a.8xlarge), GOGC 400

Past capacity

Past error capacity, a single relay refuses the excess as overloaded (503, or 529 on Anthropic-shaped routes, with Retry-After: 1) and keeps serving close to its highest rate: the 8-core box served 25,563 rps at 32,000 offered against a peak of 26,141, and the 16-core box 41,451 at 56,000 against 41,860. Two exceptions:
  • The four-relay group fell from 77,921 rps to 60,618 at the next step, with 9.7 % errors, as the wait on the shared store reached its highest (admission p99 123 ms).
  • On 32 cores the generators fell behind above 64,000 offered, so behaviour past that point was not measured.

Rerun it

The harness itself is not published. The setup and reading rules above describe the method in full: a fixed request mix against a mock with a known latency model, open-loop steps with warm-up and hold, overhead as client latency minus mock duration, and the knee and capacity rules applied per step.

Changes between campaigns

None yet: v0.11.7 is the first published campaign.