If you've ever had Suspense boundaries working perfectly in development but silently doing nothing in production, this might be why.
I ran into this recently after migrating a Next.js application to React Server Components and streaming. Locally, everything worked as expected: Suspense boundaries triggered their fallbacks, skeletons appeared immediately, streamed content loaded progressively. In production, they didn't.
What I was seeing
The behaviour split cleanly along navigation type, which was the first useful clue.
On hard navigations (a route change or path parameter change in the URL), I'd see a loading skeleton. That was loading.tsx triggering, which is expected behaviour.
On soft navigations, where only a query parameter changed rather than the path, it was different. You could see the server interaction happening: UI components would hit their disabled states on the client, which told me a server action had fired. But no skeleton appeared. No Suspense fallback. There was just a noticeable delay, and then the new content arrived as if it had teleported in.
The key prop rabbit hole
Some research turned up a well-documented issue: for Suspense fallbacks to trigger on soft navigations, you need to pass the current query parameters as a hash to the key prop on the Suspense component. Without it, React doesn't know the boundary needs to re-suspend when search params change.
<Suspense key={searchParamsHash} fallback={<Skeleton />}>
<Results />
</Suspense>I implemented this. It made no difference.
That's when the investigation changed direction. If the correct React-level fix wasn't working, something else was interfering.
Reading the network tab differently
I started looking more closely at the RSC payload requests. A quick note on what these are: when you navigate client-side in a Next.js app using React Server Components, the browser doesn't reload the page. Instead, it fetches a serialised representation of the new server component tree. These requests carry a _rsc query parameter, an RSC: 1 request header, and a vary: RSC response header. They're how you identify them in the network tab.
For streaming to work correctly, the server should start sending data almost immediately and continue sending it progressively as rendering completes. Think of it like a tap: you want water flowing the moment you turn it, even if the full flow takes a moment to build. What you don't want is for something to collect the whole thing before passing it on.
In Chrome DevTools, each network request shows a timing breakdown. The green bar represents time to first byte: how long until the server sends anything at all. The blue bar represents download time: how long the actual transfer takes.
For streaming, you want a short green bar and a long blue bar. What I was seeing was the opposite: an extremely long green bar, and a blue bar that was almost nothing. The full response was arriving almost instantly, but only after a very long wait.
That's not streaming. That's buffering.
The CDN was collecting the full response before forwarding it
CDNs buffer responses by default. They collect the full response from the origin server, then forward it to the client as a single payload. For static assets, this is sensible behaviour. For a streaming HTTP response, it completely defeats the point.
For RSC payloads specifically, this meant the CDN was waiting for the entire server component tree to finish rendering before sending a single byte to the browser. React had no opportunity to flush early chunks. Suspense boundaries never got the signal to resolve their fallbacks.
Once I had the DevTools evidence, I had a clear direction. The fix needed to target RSC requests specifically, using the identifiers Next.js attaches to them: the _rsc query parameter, the RSC request header, and the vary: RSC response header. Scoping it precisely mattered — disabling buffering globally would have had performance implications for everything else.
Two changes were needed: disabling response buffering for matching requests so the CDN forwards bytes as they arrive from the origin, and disabling compression for the same requests, since compression requires collecting the full response before it can be calculated.
After the fix
The DevTools told the story immediately. The green bar shrank to almost nothing. The blue bar stretched out. And for the first time in production, Suspense fallbacks appeared and resolved visibly as the streamed response came in.
Why this is hard to find
The tricky thing about CDN buffering is that it doesn't break anything in an obvious way. Pages load. Data arrives. Nothing throws an error. It silently removes the experience that streaming is supposed to provide, and the React-level fixes you'd reach for first appear not to work, because the problem isn't in React at all.
The Next.js self-hosting documentation has improved considerably in the last year or two, but it still doesn't go as far as telling you what specifically needs to change for particular CDN configurations. The gap is real:
"When self-hosting, you are responsible for configuring your CDN, load balancer, and server to work with Next.js streaming."
If you're self-hosting behind a CDN and Suspense fallbacks aren't appearing in production, the network tab is where to start. A long green bar on RSC requests is the tell.