App Router: SPA navigation via <Link> hangs 15-20s on 2nd visit behind Traefik reverse proxy #94040
Replies: 2 comments
-
|
This screams HTTP/2 connection reuse issue with Traefik, not an App Router bug. The fact that it only happens on 2nd visit and only behind Traefik is the giveaway. Here's what's likely happening: On the first Things to try: 1. Check Traefik's HTTP/2 settings # traefik.yml or dynamic config
serversTransport:
maxIdleConnsPerHost: 0 # try disabling connection pooling to backendOr force HTTP/1.1 between Traefik and your Next.js container: serversTransport:
disableHTTP2: trueThis isolates whether HTTP/2 multiplexing between Traefik and Next.js is the problem. 2. Separate the SSE connection Your RealtimeProvider SSE channel shares the same origin, so the browser multiplexes it on the same HTTP/2 connection as RSC requests. A long-lived SSE stream can interfere with flow control on that connection. Try putting the SSE endpoint on a different subdomain or port to force a separate connection. 3. Add response buffering in Traefik Traefik may be streaming the RSC response and waiting for a signal that never comes: middlewares:
buffering:
buffering:
maxResponseBodyBytes: 104857604. Debug the actual stall Open Chrome DevTools → Network → filter by Fetch/XHR during the hang. You should see a pending request to the RSC endpoint. Check:
My bet is on the Traefik → Next.js leg. The request reaches Traefik but gets stuck on the HTTP/2 connection to the backend. |
Beta Was this translation helpful? Give feedback.
-
|
This is a Traefik HTTP/2 connection multiplexing issue — not a Next.js bug. Here's the root cause and a prioritized fix list. Root CauseNext.js App Router's The 15-20 second hang matches Node.js's default HTTP/2 stream timeout. What's happening:
Fix Priority OrderFix 1 (fastest, test first): Disable HTTP/2 on Traefik → Next.js leg # traefik.yml or docker-compose labels
serversTransport:
disableHTTP2: true # Force HTTP/1.1 between Traefik and backend
maxIdleConnsPerHost: 100
forwardingTimeouts:
responseHeaderTimeout: 60sThis is the most impactful change. HTTP/1.1 between Traefik and Node.js doesn't have the multiplexing race condition. Fix 2: Set # Traefik middleware / service config
http:
services:
nextjs:
loadBalancer:
servers:
- url: "http://nextjs:3000"
responseForwarding:
flushInterval: 1ms # Don't buffer streaming RSC responsesFix 3: Prevent SSE connections from starving RSC streams If you're using SSE (or WebSockets) for real-time features, they keep HTTP/2 connections alive indefinitely. Move them to a separate service/subdomain so they don't share the same connection pool as RSC prefetch requests: # Route SSE to a dedicated endpoint
http:
routers:
sse:
rule: "Host(`app.example.com`) && PathPrefix(`/api/sse`)"
service: nextjs-sse
main:
rule: "Host(`app.example.com`)"
service: nextjsFix 4: Adjust Next.js // next.config.ts
export default {
experimental: {
staleTimes: {
dynamic: 0, // Don't attempt RSC revalidation on 2nd visit for dynamic routes
static: 300,
},
},
};This reduces the frequency of RSC refetch requests but doesn't fix the underlying connection issue. Fix 5: Exclude Next.js internal paths from middleware If you have auth middleware, ensure it doesn't intercept // middleware.ts
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|api/public).*)',
],
};VerificationTo confirm it's the Traefik→Next.js leg and not something else, add this to your container: NEXT_PRIVATE_DEBUG_CACHE=1Then watch logs during the hang — if you see RSC cache miss logs only after the 15-20s timeout, it confirms Traefik is holding the request before Next.js ever sees it. If you see the request arrive immediately, the issue is inside Next.js. Start with Fix 1 — it resolves this for most setups. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Environment
Description
In a multi-tenant SaaS app (App Router, ~30 routes, 4 nested client providers), client-side navigation via
<Link>works perfectly on the first visit to any route. On the second visit to a previously-visited route, the router transition hangs for 15-20 seconds — the URL does not update,usePathname()stays stale, and the page appears frozen. After the timeout the navigation eventually completes.Switching
<Link>to<a>(hard navigation) eliminates the issue entirely.Key observations
localhost:3000access in the same Docker container does not exhibit the bug.<Link>is instant. Returning to a cached route triggers the hang.What we tried
<Link>elements — still hangsCache-ControlorVaryheader differences between direct and proxied responsesPartial repro (does NOT reproduce)
We built a minimal repro with the same stack delta:
<Link>sidebar navigationRepo: https://github.com/ShivSync/nextjs-router-stall-repro
Live demo: https://nextjs-bug.meridiansuite.ai
The repro navigates instantly on every visit. The production app (same infra, same proxy) hangs on 2nd visit.
Delta between repro and production
The production app has:
Promise.allof 10+ queries in server components[id]routes withgenerateMetadataCurrent workaround
Replaced
<Link>with<a>in the sidebar layout for all primary navigation. This forces full page loads but eliminates the hang. Production is stable with this workaround.Request
We suspect the interaction is between the App Router prefetch/cache layer and Traefik's HTTP/2 connection handling, possibly related to RSC payload caching or flight data revalidation on 2nd visit. We'd appreciate any pointers on:
ConnectionorTransfer-Encodingheaders?NEXT_DEBUG_LOGGING)?Happy to provide Traefik configs, HAR captures, or any additional debugging output that would help isolate this.
Beta Was this translation helpful? Give feedback.
All reactions