Go API performance against the legacy API
A client-ready comparison of throughput, latency, and memory from 46 controlled load-test runs against the Go and legacy Next.js API implementations.
The Go API materially improves the cost and speed of serving StatsHub requests, but the size of the improvement depends on where the request spends its time. For runtime-bound work, Go serves between 4.2× and 39.1× as many requests per second in this test. A small database read reaches 8.4× higher peak throughput. Queries dominated by Postgres converge, and one aggregate query remains slower in Go.
The clearest resource result is memory: the Go process finished the test at 262 MB resident memory, compared with 2,095 MB for the legacy Next.js server. That is an 87.5% reduction, or almost exactly 8× less memory per API instance.
Download the raw CSV
All 46 measured rows: concurrency, requests, errors, throughput, p50, p90, p99, and maximum latency.
Read the engineering analysis
The endpoint-by-endpoint interpretation, saturation behavior, and benchmark harness.
Read the measured query analysis
The Postgres findings behind the database-bound results and the next optimization priorities.
Executive results
| Measure | Go API | Legacy API | Result |
|---|---|---|---|
| Resident memory after the run | 262 MB | 2,095 MB | 87.5% less |
| Peak health-check throughput | 24,833 req/s | 5,206 req/s | 4.8× higher peak |
| Health throughput at one connection | 11,323 req/s | 289 req/s | 39.1× higher |
| Health p50 latency at eight connections | 0.3 ms | 3.8 ms | 12.7× lower |
| Peak small-read throughput | 14,096 req/s | 1,673 req/s | 8.4× higher |
| Successful health requests across all levels | 747,011 | 88,482 | Zero failures on both |
| Successful small-read requests across all levels | 426,272 | 38,581 | Zero failures on both |
What the measurements support
The Go service provides substantially more API capacity per instance and needs about one eighth of the memory. The strongest gains appear when application runtime overhead is a meaningful part of the request. This supports fewer or smaller API containers for the same runtime-bound traffic.
Runtime-bound throughput
/api/health does not query the database. It isolates the cost of accepting a
request, routing it, building a response, and writing it back to the client.
Go reaches its throughput ceiling at 32 concurrent connections and remains near
that ceiling through 2,048 connections without errors.
The multiplier narrows as the legacy server fills its event-loop queue, but Go still serves 4.2× more requests per second at 2,048 connections. At one connection, before batching or deep queueing can help either runtime, the gap is 39.1×.
| Concurrency | Go req/s | Legacy req/s | Go advantage |
|---|---|---|---|
| 1 | 11,323 | 289 | 39.1× |
| 8 | 22,913 | 922 | 24.9× |
| 32 | 24,833 | 1,714 | 14.5× |
| 128 | 21,725 | 2,456 | 8.8× |
| 512 | 21,938 | 3,886 | 5.6× |
| 2,048 | 21,650 | 5,206 | 4.2× |
Runtime-bound latency
Higher throughput does not come from making callers wait in a deeper queue. Go also holds a materially lower median response time at every tested load level.
At 2,048 concurrent connections, the Go median is 89.9 ms versus 386.6 ms for legacy. At eight connections, a more representative shallow queue, the medians are 0.3 ms and 3.8 ms.
Small database reads
/api/models adds a small Postgres query. The database reduces the runtime-only
margin, but Go still reaches 14,096 requests per second compared with the
legacy server's peak of 1,673.
At eight connections, Go returns the small read with a 0.5 ms p50. Legacy takes 8.3 ms, a 16.6× latency difference. Both implementations complete every request at every level, so this comparison is not inflated by failed work.
Memory footprint
The measured difference is 1,833 MB per running instance. If a deployment kept four equivalent API instances, the same ratio would represent about 7.3 GB less resident memory. That projection assumes each instance behaves like the measured processes; it is a capacity-planning illustration, not a cloud-billing quote.
CPU usage was not sampled during this run, so this report does not claim a CPU percentage reduction. Throughput shows how much work each server completed, but it is not a substitute for per-process CPU telemetry.
Database-bound requests
The runtime stops deciding performance when Postgres dominates the request. Both implementations use a ten-connection database pool, so requests beyond ten in-flight queries wait for the same limited resource.
At eight concurrent connections:
| Endpoint | Go req/s | Legacy req/s | Go p50 | Legacy p50 | Finding |
|---|---|---|---|---|---|
| Indexed search | 15.0 | 12.9 | 365.7 ms | 456.2 ms | Go is modestly faster. |
| Referee aggregate | 5.4 | 12.0 | 1,365.5 ms | 635.7 ms | Go is slower; the query needs optimization. |
This is the main trade-off. Replacing the runtime removes application overhead; it cannot make an expensive SQL plan cheap. The referee result is a measured regression, not an outlier hidden from the headline. It is also actionable: the query analysis separates runtime work from database and indexing work.
At 512 concurrent search requests, both services cross the client's 30-second timeout. The Go run records 322 failed requests and legacy records 238. Neither process dies; both are queueing behind the database. The health endpoint still answers after saturation.
Test design
The benchmark ran on 2026-08-28 with the following controls:
| Variable | Setting |
|---|---|
| Host | 16 CPU cores, 61 GB memory |
| Network | Loopback; client and both servers on the same host |
| Database | Same local Postgres mirror for both implementations |
| Database pool | Ten connections per implementation |
| Targets | Go on port 8080; legacy Next.js API on port 3000 |
| Order | One target at a time to avoid direct CPU competition |
| Warm-up | Eight requests before each endpoint run |
| Load levels | 1, 8, 32, 128, 512, and 2,048 keep-alive connections |
| Duration | Six seconds per level |
| Client timeout | 30 seconds |
| Recorded fields | Requests, successes, failures, throughput, p50, p90, p99, maximum latency |
The four endpoints deliberately progress from no database work to an aggregate query. This separates runtime capacity from database capacity instead of blending them into one average that would explain neither.
Limits of the evidence
- Client and servers shared one host. At high concurrency, the load generator competed for CPU with the target under test.
- The local Postgres mirror served other services. Absolute query latency is less portable than the side-by-side comparison.
- Six-second levels establish throughput plateaus and saturation behavior. They do not establish long-term garbage-collection behavior or memory leaks.
- Resident memory was sampled after the run, not continuously at each load level. CPU was not sampled.
Client conclusion
The Go migration delivers a clear infrastructure benefit for the API layer: about one eighth of the memory, 4.2× to 39.1× the runtime-bound throughput, and substantially lower median latency under every tested runtime-only load. Small database reads retain a large advantage.
The remaining performance work is concentrated rather than systemic. Complex queries need SQL and database-cache improvements, especially the referee aggregate. That distinction is useful operationally: the service runtime is no longer the general bottleneck, and the next gains can target measured query paths instead of scaling every API container.
Legacy web access gates
A source audit of which legacy StatsHub pages required payment, login, or accepted a limited guest experience.
Website rebuild performance against legacy
A client-ready comparison of HTML throughput, latency, payload size, saturation, and memory across 91,909 requests to five matched pages.