radiusward benchmarks
radiusward is an open-source RADIUS server that replicates accounting over a three-member cluster: an Accounting-Response goes out only once the record is stored with fsync on a majority, so a crashed node loses nothing that was acknowledged. This page shows how much load it carries, how fast it answers, and what a crash of the cluster leader costs under load — measured with the same open harness in every environment.
How a result is measured: the request rate rises step by step. A step passes only if no request is lost, 99 % of the answers come within 500 ms (p99) and at least 90 % of the target rate is answered. The highest passing rate is the ceiling; "≥" marks a lower bound, where the limit lies higher.
github-ubuntu-24.04 · 4 vCPU x86_64
2026-11-11T18:07:17+00:00 · commit cea5e67 · openjdk version "25.0.5" 2026-10-20 LTS · profile small · raw JSON
One shared machine, 4 vCPU: three cluster members, two gateways, PostgreSQL with the exporter and 4 load generators all ran on it, each member and gateway limited to one CPU. The numbers are a lower bound and a trend from week to week; separate machines carry more.
- committed on two of three members with fsync before each Accounting-Response
- p99 302 ms, p50 12 ms
- the next step failed: p99 609 ms, over 500 ms
- Access-Requests with PAP, CHAP and MS-CHAPv2, decided by the gateways
- p99 200 ms, p50 0.3 ms
- every step passed: the limit is higher
- 70 % accounting, 30 % authentication
- p99 463 ms, p50 31 ms
- the next step failed: p99 708 ms, over 500 ms
- the cluster leader killed at 2,611 accounting requests/s
- 0 retransmissions, longest wait 2,528 ms
- every acknowledged record kept
Memory at the highest load: each gateway at most 384 MiB, each cluster member at most 619 MiB (profile small, which fits a member in 1 GiB).
Accounting: 3,731/s
Start and Interim records of fresh sessions through both gateways
CPU and memory per process at 3,750/s, the ceiling (100 % = one core)
| Process | CPU avg | CPU max | Memory |
|---|---|---|---|
| bench-gen-0 | 6 % | 28 % | 73 MiB |
| bench-gen-1 | 6 % | 19 % | 75 MiB |
| bench-gen-2 | 9 % | 50 % | 73 MiB |
| bench-gen-3 | 6 % | 26 % | 72 MiB |
| gateway-a | 60 % | 98 % | 322 MiB |
| gateway-b | 59 % | 100 % | 320 MiB |
| node-0 | 74 % | 99 % | 497 MiB |
| node-1 | 47 % | 79 % | 312 MiB |
| node-2 | 53 % | 79 % | 333 MiB |
| postgres | 16 % | 26 % | 124 MiB |
Every step, by rate
After the first failed step the harness bisects between the last good and the failed rate, so the run order (#) is not ascending. A step passes with nothing lost, p99 under 500 ms and at least 90 % of the rate answered.
| Target | Answered | p99 | # | Generators | Result |
|---|---|---|---|---|---|
| 500/s | 497/s | 163 ms | 1 | 4 | ✓ passed |
| 1,000/s | 996/s | 168 ms | 2 | 4 | ✓ passed |
| 2,000/s | 1,996/s | 209 ms | 3 | 4 | ✓ passed |
| 3,000/s | 2,996/s | 323 ms | 5 | 4 | ✓ passed |
| 3,500/s | 3,493/s | 267 ms | 6 | 4 | ✓ passed |
| 3,750/s | 3,731/s | 302 ms | 7 | 4 | ✓ passed — the ceiling |
| 4,000/s | 3,995/s | 609 ms | 4 | 4 | ✗ p99 609 ms, over 500 ms |
Authentication: ≥ 15,053/s
Access-Requests with PAP, CHAP and MS-CHAPv2
CPU and memory per process at 16,000/s, the ceiling (100 % = one core)
| Process | CPU avg | CPU max | Memory |
|---|---|---|---|
| bench-gen-0 | 25 % | 65 % | 412 MiB |
| bench-gen-1 | 23 % | 65 % | 447 MiB |
| bench-gen-2 | 30 % | 94 % | 425 MiB |
| bench-gen-3 | 24 % | 58 % | 410 MiB |
| gateway-a | 72 % | 107 % | 381 MiB |
| gateway-b | 70 % | 105 % | 381 MiB |
| node-0 | 17 % | 63 % | 483 MiB |
| node-1 | 18 % | 75 % | 327 MiB |
| node-2 | 15 % | 73 % | 331 MiB |
| postgres | 1 % | 4 % | 125 MiB |
Every step, by rate
After the first failed step the harness bisects between the last good and the failed rate, so the run order (#) is not ascending. A step passes with nothing lost, p99 under 500 ms and at least 90 % of the rate answered.
| Target | Answered | p99 | # | Generators | Result |
|---|---|---|---|---|---|
| 500/s | 499/s | 44 ms | 1 | 4 | ✓ passed |
| 1,000/s | 999/s | 38 ms | 2 | 4 | ✓ passed |
| 2,000/s | 1,988/s | 48 ms | 3 | 4 | ✓ passed |
| 4,000/s | 3,946/s | 110 ms | 4 | 4 | ✓ passed |
| 8,000/s | 7,722/s | 155 ms | 5 | 4 | ✓ passed |
| 16,000/s | 15,053/s | 200 ms | 6 | 4 | ✓ passed — the ceiling |
Mix: 6,821/s
70 % accounting, 30 % authentication
CPU and memory per process at 7,000/s, the ceiling (100 % = one core)
| Process | CPU avg | CPU max | Memory |
|---|---|---|---|
| bench-gen-0 | 20 % | 64 % | 193 MiB |
| bench-gen-1 | 14 % | 37 % | 209 MiB |
| bench-gen-2 | 16 % | 39 % | 203 MiB |
| bench-gen-3 | 13 % | 32 % | 207 MiB |
| gateway-a | 63 % | 92 % | 376 MiB |
| gateway-b | 62 % | 75 % | 383 MiB |
| node-0 | 65 % | 84 % | 601 MiB |
| node-1 | 44 % | 77 % | 381 MiB |
| node-2 | 46 % | 82 % | 389 MiB |
| postgres | 23 % | 34 % | 183 MiB |
Every step, by rate
After the first failed step the harness bisects between the last good and the failed rate, so the run order (#) is not ascending. A step passes with nothing lost, p99 under 500 ms and at least 90 % of the rate answered.
| Target | Answered | p99 | # | Generators | Result |
|---|---|---|---|---|---|
| 500/s | 493/s | 184 ms | 1 | 4 | ✓ passed |
| 1,000/s | 997/s | 229 ms | 2 | 4 | ✓ passed |
| 2,000/s | 1,992/s | 211 ms | 3 | 4 | ✓ passed |
| 4,000/s | 3,948/s | 381 ms | 4 | 4 | ✓ passed |
| 6,000/s | 5,687/s | 264 ms | 6 | 4 | ✓ passed |
| 7,000/s | 6,821/s | 463 ms | 7 | 4 | ✓ passed — the ceiling |
| 7,500/s | 7,153/s | 708 ms | 8 | 4 | ✗ p99 708 ms, over 500 ms |
| 8,000/s | 7,572/s | 1,019 ms | 5 | 4 | ✗ p99 1,019 ms, over 500 ms |
The gateway's packet path on one core (JMH)
Per accounting packet of about 200 bytes, on one core of this machine, without the network and without the cluster commit. Short runs on a shared machine are noisy; the trend below shows them over time.
| Step | What it does | Per packet | Per second, one core | Error |
|---|---|---|---|---|
fullReceivePath | all of the above: what a gateway does per accounting packet before the cluster commit | 771 ns | 1,297,000/s | ± 6 % |
validate | structure check of the packet and its attributes | 38 ns | 26,229,000/s | ± 92 % (noisy) |
verifyAuthenticator | the Request Authenticator (MD5 over the packet and the shared secret) | 451 ns | 2,216,000/s | ± 93 % (noisy) |
extract | the attributes accounting needs, read without allocating | 186 ns | 5,371,000/s | ± 153 % (noisy) |
encodeResponse | the Accounting-Response, signed | 229 ns | 4,370,000/s | ± 79 % (noisy) |
Trend
Every run per environment, to see whether a change made radiusward slower. On a shared machine single runs vary; read the direction, not one point.
github-ubuntu-24.04
runs 1: 2026-11-11 9b0d1c6, 2: 2026-11-11 cea5e67
More evidence and reproducing it
The same properties under faults: fault injection (power loss with LazyFS, partitions, full disk, a PostgreSQL outage), Kubernetes (leader killed, rolling updates, a volume replaced, a lost quorum restored), end-to-end and integration tests.
Every number here comes from scripts/bench.py; the raw results are linked per run. On your own machine
(details,
ADR 0066):
./mvnw -q install -DskipTests && docker build -f deploy/docker/Dockerfile -t radiusward:dev . scripts/bench.py # the compose stack on this machine scripts/bench-aws.sh --profile small --region <r> # on Amazon ECS, generators on their own instances