DEV Community

Cover image for Nuxt 4.5 SSR Streaming: The Route Rules That Disable It
Parsa Jiravand
Parsa Jiravand

Posted on Originally published at bestpractic.org

Nuxt 4.5 SSR Streaming: The Route Rules That Disable It

Your team flips experimental.ssrStreaming: true in nuxt.config.ts, loads the homepage, and Time to First Byte drops from 1.8 seconds to 40 milliseconds. Everyone's thrilled. Someone ships it to every route in the app. Two days later, a teammate asks why the pricing page — behind a cache route rule, same layout, same components — didn't get any faster. Then a third teammate reports pages crashing in production with an error nobody on the team has seen before: ERR_HTTP_HEADERS_SENT.

Nothing here is a Nuxt bug. It's six route rules quietly opting themselves out of streaming, and one very normal pattern — a request interceptor that sets a cookie — running straight into the one thing streaming doesn't let you do anymore: change your mind about the response after you've already sent the start of it.

This article is written against Nuxt 4.5 (verified against the 4.5.2 release on npm's latest tag; experimental.ssrStreaming shipped in 4.5.0, released July 18, 2026). Nuxt 3 reached end-of-life on July 31, 2026, so if you're still on it, this feature doesn't exist for you yet — it's 4.5-only, and still explicitly experimental: opt-in, and the options around it (we'll get to botRegex) are young enough that the Nuxt team is still renaming them for clarity.

What you'll learn

  • What experimental.ssrStreaming actually changes about the response Nuxt sends
  • Why six specific route rules — redirect, cache, isr, swr, noScripts, ssr: false — silently fall back to the old buffered renderer
  • How to opt a route out of streaming yourself with routeRules, and how Nuxt protects crawlers from it automatically
  • The real failure mode when code tries to mutate the response after the shell has flushed, and how to audit for it before you turn streaming on broadly
  • How this connects to the rendering modes and route rules you already use in Nuxt

Who this is for

You've deployed a Nuxt app, you've used routeRules in nuxt.config.ts for at least one of cache, prerender, or ssr: false, and you know roughly what "Nuxt renders on the server, then hydrates on the client" means. If that last part is fuzzy, read the hydration mismatch episode first — this article assumes you already have that mental model and builds the streaming layer on top of it.

Table of contents

The problem: one flag, and half the app doesn't change

Here's the naive version of what that team did:

// nuxt.config.ts — the "just turn it on" approach
export default defineNuxtConfig({
  experimental: {
    ssrStreaming: true,
  },
})
Enter fullscreen mode Exit fullscreen mode

It works exactly as advertised on a plain page: no route rule, no redirect logic, nothing fancy — just a component tree that fetches some data and renders. Time to First Byte on that page drops hard, because Nuxt no longer waits for the whole page to finish rendering before it sends anything.

Then someone checks /pricing, which has this in the same config file:

export default defineNuxtConfig({
  experimental: {
    ssrStreaming: true,
  },
  routeRules: {
    '/pricing': { cache: { maxAge: 60 } },
  },
})
Enter fullscreen mode Exit fullscreen mode

Same flag, same app, zero difference in TTFB. Not a regression — Nuxt silently buffered that route instead of streaming it, because cache is one of six route rules that disable streaming automatically. Nobody configured that. It isn't documented in the obvious place you'd look (the route rule itself). It's a consequence of what streaming is, and once you see that, the whole fallback list stops looking arbitrary.

The mental model: commit now vs. decide, then commit

The mental model: SSR streaming doesn't change what Nuxt renders. It changes when the response is allowed to be final.

  • Buffered SSR (the default, every Nuxt app until 4.5): Nuxt renders the entire page to a string in memory first. Only once that's done does it decide the final HTTP status code, the headers, any cookies, and send the whole thing in one response. Nothing reaches the browser until Nuxt is completely finished deciding.
  • Streaming SSR (experimental.ssrStreaming): Nuxt renders the outer shell — your root layout, the <head>, anything above the first async boundary — and the moment that's ready, it commits the status code and headers and flushes them to the socket immediately. Then it keeps writing the rest of the body to that same open connection as the remaining components finish rendering.

That's the entire feature. The speed win is real: the browser gets bytes — and can start painting and fetching sub-resources — while Nuxt is still working on the rest of the page. But committing the headers early has a one-way-door property: once the shell has flushed, Nuxt cannot change its mind about the response. No new status code, no new header, no new cookie, no "actually, redirect instead."

Read the fallback list through that lens and every entry explains itself: each of the six rules needs to make a decision about the whole response that can only be made correctly before anything is sent. Streaming removes the "before" — so Nuxt just doesn't stream those routes.

Turning it on

The flag lives under experimental because the feature is new and still finding its edges:

export default defineNuxtConfig({
  experimental: {
    ssrStreaming: true,
  },
})
Enter fullscreen mode Exit fullscreen mode

Key concept: this is a global switch, but "global" doesn't mean "every route behaves identically" — it means every route is now eligible for streaming, and Nuxt decides per-request whether a given route actually qualifies.

The six route rules that disable streaming

Per the official 4.5 release notes, routes carrying any of these routeRules automatically fall back to the buffered renderer, with no warning and no error:

Route rule Why it can't stream
redirect A redirect is a different status code and a Location header decided for the whole response. Once the shell has flushed with a 200, Nuxt can no longer turn it into a 30x.
cache Caching a response means caching a complete, final payload. You can't cache "half a page plus a promise to finish it later" in any sane way.
isr Incremental Static Regeneration writes a finished HTML artifact to disk/CDN. Same problem as cache, with a build artifact instead of an in-memory cache entry.
swr Stale-while-revalidate still needs one complete response to serve as "stale" while the real one regenerates — it's the cache problem again, with an extra background step.
noScripts This rule exists to produce fully static, no-hydration-JS output. Streaming's entire value proposition is progressive rendering as the app hydrates — there's no app to progressively hydrate here.
ssr: false This forces SPA mode for the route: the server sends a near-empty shell and the client renders everything. There's no server-rendered body to stream in the first place.

Notice the pattern: every rule on this list needs to produce one finished, final thing — a cached payload, a static file, a redirect, a client-only shell — and streaming's whole mechanism is sending an unfinished thing on purpose. The two ideas are structurally incompatible, not just awkwardly combined.

Protecting crawlers, and opting out yourself

Streaming has one more built-in exception that isn't a route rule: bots and crawlers get the buffered response too, automatically, so search engines still receive one complete HTML document instead of a stream they may not handle well:

export default defineNuxtConfig({
  experimental: {
    ssrStreaming: {
      // tune which user agents count as crawlers
      botRegex: /googlebot|bingbot|my-internal-crawler/i,
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

And if you have a route that qualifies for streaming by the rules above, but you don't trust it yet — maybe it runs business logic you haven't audited (more on that next) — you can force it to stay buffered with its own route rule:

export default defineNuxtConfig({
  routeRules: {
    '/checkout/**': { streaming: false },
  },
})
Enter fullscreen mode Exit fullscreen mode

Key concept: the six-rule fallback list is Nuxt protecting you automatically. streaming: false is you protecting yourself manually, for routes the automatic list doesn't know are risky.

What breaks when something mutates the response too late

This is the part the release notes describe as a caveat and real apps discovered as an outage. In 2026, the community auth module nuxt-auth-sanctum shipped a response interceptor that — like a lot of auth and session code — tried to set a cookie on the outgoing response during SSR. Under streaming, that interceptor ran after the shell had already flushed. The result, reported as issue #669: every SSR request on every page using the module threw ERR_HTTP_HEADERS_SENT, the render died mid-stream, and the visitor got back an HTTP 200 with a dead, partially-written page — not even a clean error, because the status code was already committed as a success before anything went wrong. The maintainers shipped a fix in PR #702 that made the module streaming-aware.

That's the general shape of the failure, and it isn't specific to that one module: anything that tries to set a cookie, a header, or a status code from inside your render — an auth interceptor, an A/B testing plugin, a geo-redirect check — is a candidate for this exact crash, the instant you turn streaming on for a route it runs on.

Edge cases and gotchas

  • Nested route rules compound. If a parent path carries cache and a child path doesn't override it, the child inherits the fallback too. Audit route rule inheritance before assuming a specific page streams — check what matches its full path, not just the rule you wrote for it.
  • This is still experimental. The Nuxt team is still refining this feature after shipping 4.5.0 — issue #36250 flagged that botRegex's name doesn't make clear which way the match goes (exclude bots from streaming, as it turns out). It was resolved by clarifying the docs rather than renaming the option, but the discussion shows the shape and naming are still being questioned this soon after release. Expect wording and defaults to keep moving through the 4.x line; re-check the docs before you lock a specific option name into muscle memory.
  • Caching and route rules got a real security patch, too. Nuxt 4.5.1 fixed a cross-user payload disclosure affecting cache, swr, and isr route rules, alongside a route rule authorization bypass and a separate server-island RCE. If you use any of those three rules, make sure you're past 4.5.1 — and if you were caching behind a CDN before upgrading, purge it, since a leaked _payload.json could already be sitting in that cache. This is the same family of bug as the cross-request state leak episode: one user's server-rendered data ending up in another user's response.
  • Streaming doesn't make a cached route faster. If a route already serves from cache/isr/swr, it was likely already fast — streaming has nothing to add there and nothing to take away; it simply doesn't apply.
  • A route can go in and out of the fallback list at runtime. If a route rule is applied conditionally (some Nuxt setups compute routeRules per-environment or per-deploy), a route that streamed in staging can silently stop streaming in production. Diff your resolved route rules, not just your source file, if TTFB numbers don't match across environments.

Best practices

  • Turn it on for one route group first, watch TTFB and your error rate for a day, then widen it. Don't flip it globally based on one benchmark page.
  • Audit anything that touches the response during render — auth modules, A/B testing, geo-redirects, custom server middleware that sets cookies — before enabling streaming on the routes they run on. If you can't audit it quickly, reach for routeRules: { streaming: false } on that path and revisit later.
  • Don't expect a route behind cache, isr, swr, redirect, noScripts, or ssr: false to get faster from this flag. It structurally can't; measure elsewhere.
  • Stay current on the 4.5.x patch line specifically, not just "on Nuxt 4" — the route-rule caching security fix landed in a patch release, not a minor.
  • Re-read the experimental features doc before each upgrade while this stays experimental; the option shape is still being adjusted.

FAQ

Does enabling experimental.ssrStreaming change my routeRules?

No. It changes how Nuxt delivers a route's response. Your routeRules are unchanged; Nuxt just reads them to decide, per route, whether it's allowed to stream or must fall back to buffering.

Why didn't my cached page get faster after I turned on streaming?

Because cache is one of the six route rules that force buffering. Streaming literally does not apply to that route — look for the TTFB win on a route with no cache/isr/swr/redirect/noScripts/ssr: false rule instead.

Can I force a route to stream even though it has a cache rule?

No — that's not a setting Nuxt exposes, and for good reason: caching a response requires one finished response to cache, which streaming doesn't produce. You can go the other direction, though: force a normally-streamable route to stay buffered with routeRules: { '/path': { streaming: false } }.

Is SSR streaming stable in Nuxt 4.5?

No. It lives under experimental in nuxt.config.ts, it's opt-in, and related option names (like botRegex) were still being refined after the initial 4.5.0 release. Treat it as something to pilot, not something to assume is final.

Does this affect what Google sees when it crawls my site?

No, by design. Nuxt detects bots and crawlers via botRegex and serves them the old buffered, fully-rendered HTML — streaming is specifically for real browsers that can render progressively.

What's the actual error if my code tries to set a cookie too late?

ERR_HTTP_HEADERS_SENT — Node's standard error for writing headers after they've already been sent. Under streaming, that moment arrives as soon as the shell flushes, which is much earlier in the request than most response-mutating code expects.

Cheat sheet

// nuxt.config.ts — SSR streaming, route rules, and the escape hatches

export default defineNuxtConfig({
  experimental: {
    ssrStreaming: {
      // bots/crawlers always get the buffered response, regardless of this regex
      botRegex: /googlebot|bingbot/i,
    },
  },

  routeRules: {
    // These six rules ALWAYS fall back to buffered rendering, automatically:
    //   redirect | cache | isr | swr | noScripts | ssr: false
    '/pricing': { cache: { maxAge: 60 } },      // buffered — caching needs a finished response
    '/old-docs': { redirect: '/docs' },          // buffered — status/header decided before render
    '/landing': { isr: 3600 },                   // buffered — writes a finished static artifact
    '/catalog/**': { swr: 300 },                 // buffered — same as cache, plus revalidation
    '/print/**': { noScripts: true },            // buffered — no hydration to stream toward
    '/legacy-app/**': { ssr: false },            // buffered — SPA shell, no server body to stream

    // Manual opt-out for a route that WOULD stream, but you don't trust yet:
    '/checkout/**': { streaming: false },
  },
})
Enter fullscreen mode Exit fullscreen mode
Route rule present Streams? Why
none ✅ Yes Nothing forces a pre-render decision
redirect ❌ No Status/header decided before body exists
cache ❌ No Needs one finished response to cache
isr ❌ No Writes a finished static artifact
swr ❌ No Same as cache, plus a revalidation step
noScripts ❌ No No hydration for streaming to serve
ssr: false ❌ No No server-rendered body at all
bot/crawler request ❌ No (forced) Crawlers get one complete document
streaming: false set manually ❌ No (your choice) You opted this route out yourself

Key takeaways

  • Streaming changes when Nuxt commits the response, not what it renders. Buffered SSR decides everything, then sends; streaming sends the shell immediately and can't take it back.
  • Six route rules — redirect, cache, isr, swr, noScripts, ssr: false — automatically fall back to buffering, because each one needs to finalize the response before streaming's "send now" moment would allow.
  • Crawlers always get the buffered version, and you can force any other route to stay buffered yourself with streaming: false.
  • Anything that mutates the response during render — cookies, headers, redirects from your own code or a module — is a crash risk under streaming once the shell has flushed; audit before widening the rollout.
  • The route rules this feature reads are the same ones that got a cross-user payload security patch in 4.5.1 — if you use cache, swr, or isr, staying current matters for more than speed.

Streaming is the first Nuxt feature that makes you think about the response as a timeline instead of a single object, and that timeline is exactly what makes the fallback list make sense instead of feeling arbitrary. Next time a route doesn't speed up the way you expected, you now know exactly which line in routeRules to go check first — and what to look for in your own code before you widen the rollout.

Have you hit the ERR_HTTP_HEADERS_SENT version of this, or a route that silently didn't stream when you expected it to? Drop it in the comments — it's early enough in this feature's life that real reports like these are still shaping how it gets documented.

🎮 Try it yourself

▶️ Open the interactive playground →

Runs right in your browser — poke at it and watch the concept react live.

🧠 Test yourself

Think it clicked? Take the 9-question quiz →

Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.

📚 Read next


🚀 Want more like this? Every guide, playground, and quiz lives on bestpractic.org — open it and sign up free so the next one finds you.

Thanks for reading! Let's stay connected:

Top comments (0)