Skip to content

perf(admin): stop the prefetch storm behind the laggy console - #368

Merged
pratyush618 merged 3 commits into
mainfrom
perf/admin-request-storm
Sep 22, 2026
Merged

pratyush618 merged 3 commits into
mainfrom
perf/admin-request-storm

Conversation

@kartikeya-27

Copy link
Copy Markdown
Contributor

The Subscribers page was showing _rsc fetches stuck on (pending) in DevTools, and the console felt laggy with it.

What the logs say

vercel logs -p byteveda-site-admin, deduplicated by request id — the CLI repeats each record once per page of results, so a raw count is 42x the truth. Fifty real requests in the 8m44s window that contains the report, one operator, sitting on one page:

Path Requests Path Requests
/inbox 11 /api/subscribers/stream 5
/subscribers 8 /api/inbox/stream 4
/stats 6 /settings 3
/members 6 /posts 3

Nobody opened /inbox eleven times. Those are prefetches of the seven rail links, re-issued over and over, and each one is a full server render that reads Postgres.

The changes

Stop the client router re-prefetching the rail. experimental.optimisticRouting defaults to true from Next 16.3.0 and is the regression behind #97135, where the prefetch scheduler livelocks. #85489 is the Next team's own confirmed "links prefetch more than once on 16", whose reporter watched Vercel invocations more than double on upgrade. #97329 is the shape that shows up as a request stuck on (pending): a prefetch whose response never settles the cache entry that asked for it.

experimental.staleTimes.dynamic defaults to 0, so a dynamic route's prefetch is stale the instant it lands and the next render is entitled to ask again. All seven rail links are dynamic, and all seven are on screen permanently. Thirty seconds is long enough that a link is fetched once per visit, and every one of these pages has a live stream or a server action behind it for the events that actually matter.

Both settings exist only because of upstream bugs, and the comment in next.config.ts names them so the next person can retest on an upgrade.

Memoise the session read. getSession() was not memoised, and a dashboard render asks for it twice — once in DashboardLayout to gate everything below it, and again in the page's own requirePermission(...). Both are right to ask; neither can assume the other ran. That was two identical three-table joins per request, and past half a session's life it was two renewal writes as well, which is the part that was a bug rather than a cost. React's cache is per-request, so a suspension or a role change still takes effect on the operator's next request.

Drop a listener nothing ever sends. No endpoint emits unavailable. The comment above it also credited a Postgres trigger for announcements the app makes itself, through subscribersChanged.

Deliberately not here

DashboardLayout reads the session and then the unread count, and that serial hop stays: the count is scoped to the workspaces the session grants, so it cannot start first. With the memoisation in place the second read is free anyway.

Folding /api/subscribers/stream into the inbox stream is real — two held invocations per open Subscribers tab instead of one — but it is nine of fifty requests against the thirty-seven the config change addresses, and it is a behaviour change across the console rather than a configuration one. Worth its own issue.

Verification

pnpm --filter @byteveda/admin typecheck, 256 tests across 26 files, pnpm lint (exit 0, nothing in the changed files), pnpm check:arch (319 files, no violations), and next build with no warning about either experimental key.

The thing still worth checking after this deploys is the production logs: /inbox should stop appearing eleven times in nine minutes.

The rail is seven links to seven dynamic routes and it never leaves the
screen, so anything that re-prefetches costs seven server renders. One
operator sitting still on one page produced eleven renders of /inbox in
eight minutes of production logs.

optimisticRouting defaults to true from Next 16.3.0 (vercel/next.js#97135,
#85489); staleTimes.dynamic defaults to 0, so a dynamic prefetch is stale
the moment it lands. Both are upstream bugs — retest on the next upgrade.
A dashboard render asks twice — the layout gates everything below it, the
page gates itself — and both are right to ask. That was two identical
three-table joins per request, and past half a session's life two renewal
writes as well.
No endpoint emits `unavailable`. The comment above it also credited a
Postgres trigger for announcements the app makes itself.
@vercel

vercel Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
byteveda-site Ready Ready Preview Sep 22, 2026 5:34pm UTC
byteveda-site-admin Ready Ready Preview Sep 22, 2026 5:34pm UTC
2 Skipped Deployments
Project Deployment Actions Updated
byteveda-site-academy Skipped Skipped Sep 22, 2026 5:34pm UTC
byteveda-site-flexiq Skipped Skipped Sep 22, 2026 5:34pm UTC

@pratyush618
pratyush618 merged commit ef6a98c into main Sep 22, 2026
16 checks passed
@pratyush618
pratyush618 deleted the perf/admin-request-storm branch September 22, 2026 17:36

This branch was successfully deployed

2 active and 2 inactive deployments
Preview – byteveda-site — c439b96d Deployed Sep 22, 2026 by vercel[bot]
Preview – byteveda-site-admin — c439b96d Deployed Sep 22, 2026 by vercel[bot]
Preview – byteveda-site-academy — c439b96d Deployed Sep 22, 2026 by vercel[bot]
Preview – byteveda-site-flexiq — c439b96d Deployed Sep 22, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants