Server Action update never commits: a transition lane stays suspended and is never pinged
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 142k
- Forks
- 32.4k
- Avg merge
- 2d 14h
- Merged PRs (30d)
- 351
Description
Link to the code that reproduces this issue
https://github.com/dakini10/next-loading-stall-repro
To Reproduce
npm install
npx next build
ITEM_COUNT=200 BLURB_REPEAT=6 PAGE_DELAY_MS=200 ./run-probe.sh mine 144 8 2
run-probe.sh clicks the Server Action button and watches whether the list ever reflects the
new order, over N parallel Playwright runs with two busy-loop processes competing for CPU, then
prints a tally. The failure is stochastic — a single run proves nothing, so please budget
n ≥ 150 on 16.3.4.
Current vs. Expected behavior
Expected: the Server Action writes a cookie, calls revalidatePath, and the page
re-renders with the new order.
Current: intermittently the page never updates. The action's POST returns 200 with a
correct payload, nothing is left in flight, no error is logged, and the old DOM stays
indefinitely (observed out to 60s). A later router.refresh() does not recover it — the new
lane suspends the same way.
Read off the FiberRoot after the action has settled:
pendingLanes=512 suspendedLanes=512 warmLanes=512 entangledLanes=512 pingedLanes=0
A transition lane is suspended and was never pinged, although the thenables the boundary
parked on have already fulfilled. actionQueue.pending is null (the router has finished);
the page's RSC chunk sits at resolved_model and is never rendered.
Rates measured
Same machine, same knobs. A run is only counted when the lane signature above is present — a
run that merely missed the 15s window on a loaded machine ends with lanes back at 0 and is
tallied separately, not as this bug.
| config | stalls / runs |
|---|---|
| 16.2.6 | 5 / 72, and 16 / 72 in an earlier batch |
| 16.3.4 | 2 / 192 |
| 16.4.0-canary.19 | 0 / 141 |
16.2.6, app/(app)/loading.jsx deleted |
1 / 132 |
#95391 (shipped in 16.3.0) clearly took a large bite out of this, but 16.3.4 still reproduces.
Three caveats, so the table is not over-read:
loading.jsraises the rate but is not strictly required. I expected removing it to take
this to zero — related reports describe it as necessary — and it did not: 1 / 132 still
carried the exact lane signature. Note the no-loading.jsarm ran at a higher load (1-min
load average 61 → 168) than the arm that produced 5 / 72 (13 → 58), so load does not explain
the reduction; if anything it understates it.- The rate moves a lot with machine load. The same 16.2.6 build measured 22% starting from
a load average around 31 and 7% starting from around 13, so batches taken hours apart are not
comparable to each other. The table establishes that 16.3.4 still reproduces, not by how
much the rate differs between versions. (A properly interleaved comparison is running; I will
add it as a comment.) - 0 / 141 on canary is not evidence that canary fixed it. At a ~1% rate, 141 runs cannot
distinguish "fixed" from "not seen yet".
What the shape needs
Four things are present in the reproduction:
loading.json a segment (the<Suspense>it creates) — see the caveat above.- The page sits one segment below that boundary (
(app)/loading.jsx+(app)/decks/page.jsx). - The layout at that boundary is
asyncand awaits on every request, so aRefreshAll
re-suspends the layout too. - The Server Action writes a cookie and calls
revalidatePath(either alone is enough to
put Next on theRefreshAllpath, viaisCookieRevalidatedin
server/app-render/action-handler.js).
Also required: a production build. next dev never reproduced it here.
I first tried the obvious minimal shape — page directly in the boundary's own segment, plain
synchronous layout — and got 0 / 24 on 16.2.6 across four variations (plain click; a
concurrent navigation; an 800ms server delay; a 200-row payload of client components). Adding
(2) and (3) produced 5 / 24 immediately, with the lane signature above. I changed those two
together, so I have not isolated which of them matters — only that the naive shape did not
reproduce and this one does. run-ablations.sh in the repo separates them.
Additional context
This came out of a real app where the same lane signature shows up at 23.1% on 16.2.6 and 2.6%
on 16.3.4 (n = 117 each), so the reduction is not an artifact of the reduction itself.
Every probe run asks the server which Next version answered it (/api/version) and fails if
that disagrees with the version under test. That check is in the repo because it caught a real
error in my own measurements: with Playwright's reuseExistingServer enabled, a killed server
outlived the port check on a loaded machine, and a whole batch got measured against the previous
version under the new label.
Related: #86151 / #95391 (fixed the bulk of this), #84299, #86055, #97036, #96233.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the reproduction repository and run its production-build probe with at least 150 runs; use run-ablations.sh to compare the segment and async-layout shapes. Read server/app-render/action-handler.js around isCookieRevalidated and inspect the reported FiberRoot lane signature. Done means the Server Action reliably updates the page without a suspended, unpinged lane.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, nextjs, playwright, react
- Domain
- full-stack
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100