Benchmarks
Reproducible throughput, latency and overhead measurements
Benchmarks
Every number on this page comes with its machine, method and spread, so you can judge it and rerun it yourself.
- Host: AMD Ryzen 5 5600H (6C/12T), CachyOS, governor
performance - Toolchain: Go 1.26.5, fasthttp v1.77.0
- Comparison servers: Fiber v2 2.52.16, Fiber v3 3.6.1, Echo v5 5.4.4
- Load generator:
wrkon the same host, so client and server share cores - Server settings:
GOMAXPROCS=12, release build, logging disabled
Because the load generator runs on the same machine, absolute throughput is lower than a two-host setup. Relative comparisons remain useful; everything below is reproducible from this repository.
How the sweeps were run
- Round-robin (
bench/fairparams_rr.sh): each rep runs every framework in sequence, so thermal drift hits all of them equally. Median of 3 reps, 10s per rep, reported with min-max. - Same-host caveat:
wrk -t12plus a 12-thread server oversubscribes a 6-core CPU. Spread of 5-15% is normal in that configuration; differences smaller than the spread are noise, not wins. - In-process measurements (no sockets, no syscalls) are deterministic and are the right way to compare framework overhead.
Dynamic route: GET /user/42
c = 512, t = 12 (oversubscribed)
| Framework | RPS (median) | min-max | p99 |
|---|---|---|---|
| fasthttp | 462,022 | 455,666-464,543 | 4.97 ms |
| fiber v2 | 453,277 | 445,724-456,400 | 5.08 ms |
| ply | 450,054 | 433,408-458,268 | 5.07 ms |
| fiber v3 | 441,530 | 440,756-450,752 | 5.25 ms |
| net/http | 296,132 | 292,545-296,678 | 9.46 ms |
| chi | 275,713 | 271,759-277,012 | 11.39 ms |
| gin | 275,065 | 258,416-277,736 | 11.69 ms |
| echo | 270,836 | 259,125-274,009 | 11.26 ms |
Ply is 2.6% behind fasthttp and inside 2% of both Fiber versions: the fasthttp-based group is interchangeable at this concurrency. Every fasthttp-based server is ~1.6x the net/http-based group.
c = 200, t = 6 (thread-matched)
| Framework | RPS (median) | min-max | p99 |
|---|---|---|---|
| fasthttp | 453,821 | 444,840-459,026 | 2.20 ms |
| fiber v2 | 446,904 | 440,859-450,891 | 2.16 ms |
| ply | 442,483 | 439,822-442,856 | 2.20 ms |
| fiber v3 | 439,446 | 431,254-443,011 | 2.24 ms |
| net/http | 285,608 | 285,363-285,989 | 3.52 ms |
| gin | 264,289 | 262,677-265,495 | 3.72 ms |
| chi | 263,415 | 261,926-266,778 | 3.68 ms |
| echo | 256,752 | 256,026-257,498 | 3.66 ms |
Within 2.5% of fasthttp and 1% of Fiber, with sub-1% spread on Ply. This is the configuration to trust for framework comparisons: parity with the other fasthttp-based servers, which is the expected result for a thin layer on the same engine.
Plaintext and JSON
We do not publish a separate plaintext/JSON table. Those workloads barely exercise routing: they measure fasthttp and the JSON encoder, not the framework, and the differences between the fasthttp-based servers sit inside run-to-run noise. The dynamic route above is the comparison that actually separates the routers.
Route lookup (CPU only, deterministic)
go test -bench on the router, no network, 200,000 iterations:
| Benchmark | ns/op | allocs |
|---|---|---|
| Static dispatch | 11 | 0 |
120 parametric routes, slot lookup (FindInto) | 45 | 0 |
| Parametric dispatch | 72-81 | 0 |
| Wildcard, compiled fast path | 73 | 0 |
| Wildcard, radix fallback | 76 | 0 |
Absolute values shift with CPU frequency between sessions, so compare within a run, not across machines. Route params land in eight inline slots on the context, so a matched request allocates nothing (0 B/op, 0 allocs/op above). Correctness is enforced by a differential fuzz test that compares the slot API against the original map-based lookup over random paths and methods.
Free per-request path parsing
fasthttp parses the request URI and normalizes the path on the first
ctx.Path() call, which dominates handler CPU for trivial routes. Ply
uses the raw request target whenever normalization provably cannot change it
and falls back otherwise (1,000,000 iterations):
| ns/op | |
|---|---|
ctx.Path() (parse + normalize) | 105 |
| Ply guarded fast path | 14 |
A 50,000-target differential test asserts the fast path is byte-identical to
ctx.Path(); queries and fragments are trimmed the same way.
Framework overhead per request (in-process, no syscalls)
Each framework is invoked as a fasthttp.RequestHandler with a reused
RequestCtx (GET /user/42), which isolates framework work from the kernel.
Median of 3 runs, 300,000 iterations each:
| Case | ns/request |
|---|---|
| bare fasthttp (path + switch) | 173 |
| Ply, param route | 180 |
| Ply, static route | 144 |
| Fiber v2 | 287 |
| Fiber v3 | 305 |
| Ply, JSON response | 605 |
The bare static baseline (63 ns) never touches the path, so it does not pay for URI parsing; the comparison that matters is the param row, where both sides must read the path. Ply is within 4% of a hand-written path-plus-switch handler, meaning the router, context pool, middleware chain and recovery together are effectively free, and ~37% thinner than Fiber on the same engine.
Reproduce
# deterministic router + handler benchmarks
go test ./internal/router -bench=. -benchtime=100000x -benchmem -run='^$'
go test . -bench='Benchmark(RoutePath|CtxPath)' -benchtime=1000000x -run='^$'
cd bench && go test ./inproc -bench=. -benchtime=300000x -run='^$'
# network sweeps (needs wrk, postgres and built bench servers)
bash bench/fairparams_rr.sh 3 10s
WRK_T=6 WRK_C=200 bash bench/fairparams_rr.sh 3 10sRaw results and profiling notes live in
bench/results/report.md.

Ply