Skip to content

Performance ​

Ada is designed for low overhead with zero heap allocations on the request hot path. This page documents benchmark results comparing ada against Echo and Gin.

Methodology ​

All benchmarks use httptest.NewRecorder + httptest.NewRequest and call ServeHTTP directly — no TCP overhead, pure router + middleware performance. Each benchmark runs with -benchmem -count=3 and reports the median result.

Environment: Go 1.26.2, Linux, AMD Ryzen 7 5800X, 16 logical CPUs. Framework versions: Echo v5.3.0 and Gin v1.12.0. Results will vary by hardware — run the benchmarks yourself for your specific environment.

Source code: _examples/benchmark/

Router Benchmarks ​

Comparison: Ada vs Echo vs Gin ​

BenchmarkAdaEchoGin
Static routes (5)98.8 ns, 0 alloc201.7 ns, 0 alloc171.6 ns, 0 alloc
Static deep /api/v1/users/list/all26.2 ns, 0 alloc49.7 ns, 0 alloc35.0 ns, 0 alloc
1 param /users/{id}39.7 ns, 0 alloc43.8 ns, 0 alloc41.3 ns, 0 alloc
3 params /api/{v}/users/{id}/posts/{pid}100.9 ns, 0 alloc66.3 ns, 0 alloc54.0 ns, 0 alloc
5 middlewares33.4 ns, 0 alloc40.9 ns, 0 alloc50.8 ns, 0 alloc

Key Takeaways ​

Routing: Ada uses a full-path compressed radix trie — '/' separators live inside the radix keys, so one comparison can match several path segments at once, and the '/' bookkeeping metadata for each key is pre-computed at registration time. This makes ada the fastest of the three on static routes and single-param routes. Param-heavy paths (3+ params) remain slower than Echo/Gin because each param requires a segment-boundary stop and a stdlib-compatible SetPathValue map write per param — the price of keeping handlers as plain http.HandlerFunc with r.PathValue support.

Middleware: Ada's middleware chain is baked at registration time. All three frameworks complete the five-middleware benchmark without heap allocations. Ada is about 18% faster than Echo v5 and 34% faster than Gin in this benchmark.

Allocations: All three frameworks achieve zero heap allocations for routing. Ada uses in-place path walking (no strings.Split) and a sorted children slice (no Go map hashing).

In practice: The routing differences (a few ns to roughly 47 ns in these scenarios) are negligible for real HTTP handlers that do 1-100 ms of actual work (database queries, API calls, JSON serialization). At 100k requests/second, the entire routing overhead is less than 1% of total CPU time. Router choice should therefore consider API design and operational features alongside these synthetic hot-path results.

Ada-Only Detailed Benchmarks ​

Benchmarkns/opB/opallocs/op
Static root /12.4400
Static short /users18.9000
Static deep /api/v1/users/list/all19.2500
1 param /users/{id}39.0600
3 params97.2900
Wildcard /files/*43.0700
50 mixed routes41.8900
200 mixed routes46.8000
404 Not Found218.1983
405 Method Not Allowed265.61234
0 middlewares18.8100
1 middleware20.5700
5 middlewares32.2000
10 middlewares45.7400
Slot (runtime reload)25.9600
Pipeline (3 entries)30.2600
Pipeline (5 entries)35.4000

Notes ​

  • Middleware scaling: 0 to 10 middlewares adds only ~27 ns because the chain is pre-built at registration time. The per-request cost is a function-call chain, not a loop.
  • Route count scaling: 50 routes to 200 routes adds only ~5 ns due to the radix trie structure — lookup is O(path length), not O(route count).
  • Slot / Pipeline overhead: ~3-5 ns over an equivalent static middleware. Both use pre-built handler chains with zero allocations. The only per-request cost is one atomic pointer load. When WithTimeout variants are active, one context derivation is added per request (~400 ns); this cost is only paid when timeout-based cancellation is in use.
  • 404/405 allocations: The remaining allocations on these paths come from stdlib http.Error / http.NotFound (header map write + body formatting). The middleware chain itself is pre-built at registration time and allocation-free per request.

Optimizations ​

Ada's router achieves its performance through several key optimizations:

  • Full-path compressed radix trie: '/' separators are part of the radix keys, so consecutive static segments compress into a single key and one memequal comparison can consume several segments. Param/wildcard alternatives anchor at segment-start nodes and are only consulted on static dead ends.
  • Sorted children slice: Trie child lookups use a sorted []staticChild slice with linear scan instead of a Go map[byte]*node. For the typical 1-4 children per node, linear scan on contiguous memory (~0.5 ns) is significantly faster than Go map hashing (~8 ns).
  • Inlined node structure: Static trie fields (StaticKey, StaticChildren) are inlined directly in the node struct, eliminating a pointer dereference per trie level and improving cache locality.
  • Pre-computed key slash metadata: The position of the last '/' and the '/' count of each radix key are computed once at registration time and stored on the node, so per-request segment bookkeeping avoids strings.LastIndexByte / strings.Count on every key hop.
  • In-place path walking: Request paths are walked byte-wise without allocating a []string slice. Wildcard values are reconstructed via substring of the original path.
  • Pre-chained error handlers: The 404/405 middleware chains are composed at registration time, not per request. The Allow header for 405/auto-OPTIONS responses is pre-computed on each node at registration.
  • Pre-built middleware chains: Middleware is composed into a single handler closure at route registration time. Per-request cost is zero — no chain resolution, no allocation, no loop.
  • Slice-based method dispatch: Per-node method handlers live in a small []methodEntry slice scanned linearly instead of a map[string]http.HandlerFunc. For the typical 1-4 methods per node, a string comparison beats map hashing. The entry also carries the route pattern and pre-computed param names, so dispatch resolves handler, pattern, and params in a single lookup with no wrapper closure.
  • Pre-built Slot/Pipeline chains: Both Slot and Pipeline pre-build handler chains at mutation time (not per-request). The hot path is one atomic pointer load (~1 ns) with zero allocations. Cancel contexts for WithTimeout variants are opt-in — only created when timeout-based cancellation is actually used.
  • Leak-free context merging: When WithTimeout is active, mergeContexts returns a cleanup function that deregisters watchers from the generation context, preventing unbounded memory growth across requests.
  • Direct method strings: HTTP methods from net/http are already uppercase per RFC 7230, so no strings.ToUpper conversion is needed.

Running Benchmarks ​

Ada-only benchmarks ​

sh
# From the repository root
go test -bench=. -benchmem -count=3 .

Framework comparison ​

sh
# From _examples/benchmark/
go test -bench=. -benchmem -count=3 .

Comparison with benchstat ​

For statistically rigorous comparison:

sh
cd _examples/benchmark
go test -bench=BenchmarkAda -benchmem -count=10 . > ada.txt
go test -bench=BenchmarkEcho -benchmem -count=10 . > echo.txt
go test -bench=BenchmarkGin -benchmem -count=10 . > gin.txt
# Use benchstat to compare (go install golang.org/x/perf/cmd/benchstat@latest)