StatsHub Docs
GeneralReports

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.

Executive results

MeasureLegacy websiteRebuilt websiteResult
Home peak without failures18.5 req/s7.6 req/sLegacy 2.4× higher
100 Club peak without failures1,007.4 req/s70.1 req/sLegacy 14.4× higher
Player Stats peak without failures790.6 req/s48.7 req/sLegacy 16.2× higher
Player detail p50, one connection26.5 ms22.4 msRebuild 15.5% lower
Fixture detail p50, one connection18.6 ms24.4 msLegacy 23.8% lower
Memory after cooldown169.5 MiB1,188.9 MiBLegacy 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.

RouteLoadLegacy p50Rebuild p50Legacy req/sRebuild req/s
Player detail126.5 ms22.4 ms35.942.3
Player detail8123.1 ms156.5 ms63.750.3
Player detail32448.4 ms602.2 ms70.352.0
Fixture detail118.6 ms24.4 ms52.739.2
Fixture detail8101.8 ms175.0 ms77.744.4
Fixture detail32393.7 ms680.0 ms81.046.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.

RouteLegacy HTMLRebuilt HTMLMultiple
Home1,050 KB2,980 KB2.8×
100 Club62 KB395 KB6.3×
Player Stats85 KB601 KB7.1×
Player detail123 KB985 KB8.0×
Fixture detail110 KB415 KB3.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:

WebsiteCache-Control
Legacypublic, max-age=300, stale-while-revalidate=3600
Rebuildprivate, 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.

RouteLegacy first timeout levelRebuild first timeout level
Home512512
100 ClubNone2,048
Player StatsNone2,048
Player detailNone2,048
Fixture detailNone2,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.

PointLegacyRebuildRebuild / legacy
Before71.2 MiB384 MiB5.4×
Immediately after171.6 MiB3,196 MiB18.6×
After cooldown169.5 MiB1,188.9 MiB7.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:

VariableSetting
Host16 CPU cores, 61.85 GiB memory
TargetsProduction-built Next.js 14 legacy container and Next.js 16 rebuild container
NetworkDirect loopback connection to each application; no TLS, Caddy, or CDN
DataSame Postgres mirror; each website uses its deployed data path
RoutesHome, 100 Club, Player Stats, one fixed player, one fixed fixture
OrderOne target at a time to avoid direct CPU competition
Warm-upEight complete requests before each route
Load levels1, 8, 32, 128, 512, and 2,048 keep-alive connections
DurationSix seconds per level, plus time to drain in-flight requests
Client timeout30 seconds
EncodingIdentity; raw HTML bytes, without compression
Recorded fieldsRequests, 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.
  1. Separate public and private rendering. Render cacheable board and entity documents independently from watchlists, sessions, locale preferences, and other reader-specific islands.
  2. 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.
  3. 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.
  4. Profile retained memory. Capture a heap snapshot after the 512-connection level and compare it with the cooled 1.16 GiB state.
  5. 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.

On this page