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.
The rebuilt website is competitive on a dynamic player page at low load, but it does not yet match the legacy site's serving capacity. The largest gaps appear on public pages that legacy can return as compact, reusable HTML while the rebuild generates a larger, private response for every request.
Across 91,909 requests to five matched pages, the two sites returned 85,064 complete HTML documents. Legacy completed 73,426; the rebuild completed 11,638. Despite that difference, the rebuild transmitted 7.90 GB, compared with legacy's 6.62 GB, because each rebuilt document is substantially larger.
Download the raw CSV
All 59 measured rows: concurrency, requests, errors, bytes, throughput, and p50/p90/p99/maximum latency.
Read the API benchmark
The matching client report for the Go and legacy API implementations.
Executive results
| Measure | Legacy website | Rebuilt website | Result |
|---|---|---|---|
| Home peak without failures | 18.5 req/s | 7.6 req/s | Legacy 2.4× higher |
| 100 Club peak without failures | 1,007.4 req/s | 70.1 req/s | Legacy 14.4× higher |
| Player Stats peak without failures | 790.6 req/s | 48.7 req/s | Legacy 16.2× higher |
| Player detail p50, one connection | 26.5 ms | 22.4 ms | Rebuild 15.5% lower |
| Fixture detail p50, one connection | 18.6 ms | 24.4 ms | Legacy 23.8% lower |
| Memory after cooldown | 169.5 MiB | 1,188.9 MiB | Legacy uses 7.0× less |
What the measurements support
The rebuild's dynamic data path can match or beat legacy latency when the queue is shallow. Its current document layer is the constraint: public responses are 3.8× to 8.0× larger on four of the five routes, public pages are marked private/no-store, and the server retains materially more memory after extreme load. Caching and response size should be addressed before increasing web capacity with more instances.
Public-page throughput
The widest gap is on routes that can be prepared once and reused. Legacy serves
/100-club at roughly 1,000 requests per second from 128 through 2,048
connections. The rebuild plateaus around 70 requests per second.
At 2,048 connections, legacy completes every /100-club request with a 2.16 s
median. The rebuild completes 2,035 of 2,048 with a 30.83 s median. Both
processes remain available after the run; the difference is queue length, not a
crash.
/player-stats has the same shape:
The zero at 2,048 is deliberate: all rebuilt requests crossed the 30-second timeout, so counting completed timeout errors as throughput would manufacture a success from failed work. Legacy completed all 6,144 responses at that level with a 2.78 s median.
Dynamic pages are closer
The player detail page is the rebuild's strongest result. At one connection it returns in 22.4 ms median, compared with 26.5 ms for legacy. It sustains 52.0 requests per second without failures at 32 connections, against 70.3 for legacy.
| Route | Load | Legacy p50 | Rebuild p50 | Legacy req/s | Rebuild req/s |
|---|---|---|---|---|---|
| Player detail | 1 | 26.5 ms | 22.4 ms | 35.9 | 42.3 |
| Player detail | 8 | 123.1 ms | 156.5 ms | 63.7 | 50.3 |
| Player detail | 32 | 448.4 ms | 602.2 ms | 70.3 | 52.0 |
| Fixture detail | 1 | 18.6 ms | 24.4 ms | 52.7 | 39.2 |
| Fixture detail | 8 | 101.8 ms | 175.0 ms | 77.7 | 44.4 |
| Fixture detail | 32 | 393.7 ms | 680.0 ms | 81.0 | 46.0 |
This is an important distinction. The rebuild is not uniformly slow, and the Go API behind it is not the general bottleneck. The low-load player result shows that its server-side data path can be faster. The larger regression starts when the application has to serialize and queue many full App Router documents.
The response is carrying more work
Every request asked for uncompressed HTML so the comparison measures the bytes each application generates before a proxy or browser compresses them.
| Route | Legacy HTML | Rebuilt HTML | Multiple |
|---|---|---|---|
| Home | 1,050 KB | 2,980 KB | 2.8× |
| 100 Club | 62 KB | 395 KB | 6.3× |
| Player Stats | 85 KB | 601 KB | 7.1× |
| Player detail | 123 KB | 985 KB | 8.0× |
| Fixture detail | 110 KB | 415 KB | 3.8× |
The byte result rules out a simple runtime comparison. The rebuild is doing more work per response: it renders the HTML shell and serializes React Server Component data for hydration. On the player detail page, that extra work still produces a lower low-load median. On the public content pages, it combines with request-time rendering and becomes the dominant cost.
Cache behavior compounds the origin result
Header probes after warm-up returned the same policy on all five routes:
| Website | Cache-Control |
|---|---|
| Legacy | public, max-age=300, stale-while-revalidate=3600 |
| Rebuild | private, no-cache, no-store, max-age=0, must-revalidate |
The benchmark connects directly to each application container, so no CDN or reverse-proxy cache answered these requests. Legacy's public header therefore does not explain its origin throughput advantage. It does mean that a cache in front of the two sites can reuse legacy documents for five minutes while it must forward every rebuilt request to the application.
The source shows why some rebuilt routes become request-time documents. The
100 Club route reads the signed-in reader's watchlist through getPrivateApi
inside the same server component that fetches the public board; that helper
reads cookies and disables caching. Public layouts also resolve locale from a
cookie and pass the selected catalogue into client providers. Personal data is
valid, but placing it in the full document path makes the public shell private.
Saturation and failures
The high levels distinguish a deep queue from a dead process. Neither site stopped responding, and both were healthy after the run.
| Route | Legacy first timeout level | Rebuild first timeout level |
|---|---|---|
| Home | 512 | 512 |
| 100 Club | None | 2,048 |
| Player Stats | None | 2,048 |
| Player detail | None | 2,048 |
| Fixture detail | None | 2,048 |
Legacy's home page records 41 timeouts at 512 connections, then times out 2,048 queued requests at the final level. The rebuild reaches the same condition at 512, so its 2,048 level is skipped. On the other four routes, legacy completes every request through 2,048 connections. The rebuild completes most player detail requests at that level, but fully times out on Player Stats and Fixture.
Memory after load
Container memory was sampled before the run, immediately after it, and after the servers had cooled without receiving benchmark traffic. These are snapshots, not a continuous peak measurement.
| Point | Legacy | Rebuild | Rebuild / legacy |
|---|---|---|---|
| Before | 71.2 MiB | 384 MiB | 5.4× |
| Immediately after | 171.6 MiB | 3,196 MiB | 18.6× |
| After cooldown | 169.5 MiB | 1,188.9 MiB | 7.0× |
The rebuild releases most of the immediate surge, but it settles about 805 MiB above its starting point. This run is too short to call that a leak. It is enough to make retained memory a measured capacity concern and to justify a heap profile during the next pass.
Test design
The comparison ran on 2026-08-29 with the following controls:
| Variable | Setting |
|---|---|
| Host | 16 CPU cores, 61.85 GiB memory |
| Targets | Production-built Next.js 14 legacy container and Next.js 16 rebuild container |
| Network | Direct loopback connection to each application; no TLS, Caddy, or CDN |
| Data | Same Postgres mirror; each website uses its deployed data path |
| Routes | Home, 100 Club, Player Stats, one fixed player, one fixed fixture |
| Order | One target at a time to avoid direct CPU competition |
| Warm-up | Eight complete requests before each route |
| Load levels | 1, 8, 32, 128, 512, and 2,048 keep-alive connections |
| Duration | Six seconds per level, plus time to drain in-flight requests |
| Client timeout | 30 seconds |
| Encoding | Identity; raw HTML bytes, without compression |
| Recorded fields | Requests, successes, failures, bytes, throughput, p50, p90, p99, maximum latency |
The fixed player and fixture ensure both applications render the same subjects. The response bodies are not byte-identical because the products do not ship the same document architecture; payload size is therefore a measured result, not a controlled constant.
What the report does not measure
- It measures the initial HTML document, not JavaScript, CSS, images, hydration, interaction latency, or Core Web Vitals in a browser.
- Identity encoding exposes server serialization cost. Real clients negotiate compression, which reduces transfer size but adds compression work somewhere in the delivery path.
- Client and servers share one host. At high concurrency, the load generator competes with the target for CPU and memory bandwidth.
- Six-second levels establish throughput and saturation. They do not establish long-term garbage-collection behavior or a memory leak.
- The legacy public cache policy may be too broad for personalized pages. The report identifies its capacity effect; it does not recommend caching private user data.
Recommended order of work
- Separate public and private rendering. Render cacheable board and entity documents independently from watchlists, sessions, locale preferences, and other reader-specific islands.
- Restore public cache semantics. Keep authenticated fragments private, but allow shared page shells and public API reads to be reused by the application and edge cache.
- Set an HTML/RSC payload budget. Start with the 100 Club and Player Stats routes, where the rebuild emits 6.3× and 7.1× the legacy bytes.
- Profile retained memory. Capture a heap snapshot after the 512-connection level and compare it with the cooled 1.16 GiB state.
- Rerun the same matrix. The checked-in harness and raw CSV make caching, payload, latency, and failure changes directly comparable with this baseline.
Client conclusion
The rebuild has a competitive dynamic data path: its player detail page is faster than legacy at one connection, and it remains within the same order of magnitude under normal concurrency. The current delivery architecture gives up that advantage by generating private documents that are several times larger.
The next performance milestone is therefore specific and measurable: make public responses reusable, reduce serialized document size, and bring the two public-page plateaus above the current 70 and 49 requests per second. Those changes target the bottleneck this run actually found rather than adding servers around it.
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.
Classic web performance convergence
What was shared with the experimental UI, what remains separate, and the measured result of the PPR, cache, Nuqs, Suspense, and client-controller pass.