Next.js 16.4.6 Now Gzips Route Handlers. I Checked Streams, Downloads and Pre-Compressed Bodies
Next.js 16.4.6 shipped overnight with a one-line release note: "Automatically compress route handler responses if not already compressed." If you self-host with next start, that's a real change. Your API routes were neve
Next.js 16.4.6 shipped overnight with a one-line release note: "Automatically compress route handler responses if not already compressed." If you self-host with next start, that's a real change. Your API routes were never gzipped before, and now they are.
Compression and streaming don't always mix, so I ran the same routes on 16.4.4, 16.4.5 and 16.4.6.
What changed
The fix is PR #101220, backported in #101280. Its description says route handler responses "were never compressed when self-hosted". Next stored single header values as one-element arrays, the compression middleware only accepts a string Content-Type, so every route handler response skipped compression. Pages were compressed all along.
How I tested
A small app built with next build and run with next start on Node 22.23.4. Requests come from a raw node:http client sending Accept-Encoding: gzip, so nothing gets decoded behind my back, plus plain fetch() to see what a normal client gets. The routes look like this one:
// app/sse/route.ts
export const dynamic = 'force-dynamic'
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
export async function GET() {
const enc = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
for (let i = 1; i <= 5; i++) {
controller.enqueue(enc.encode(`data: {"token":"chunk ${i}"}\n\n`))
await sleep(500)
}
controller.close()
},
})
return new Response(stream, {
headers: { 'content-type': 'text/event-stream', 'cache-control': 'no-cache' },
})
}
Here's what came back:
| Route | 16.4.4 | 16.4.5 | 16.4.6 |
|---|---|---|---|
Response.json() with 200 items (5,591 bytes) |
not gzipped | not gzipped | gzipped, 964 bytes on the wire |
| SSE stream above | not gzipped | not gzipped | gzipped, still streams |
600 KB application/pdf with Content-Length
|
Content-Length: 614400 |
no Content-Length, chunked |
no Content-Length, chunked |
Body gzipped by the handler, with Content-Encoding: gzip
|
works | header dropped, client gets raw gzip bytes | gzipped twice, client gets gzip bytes after decoding |
Streams still stream
This was my main worry. On 16.4.6 the SSE route came back with Content-Encoding: gzip, and the decoded events arrived at 9, 508, 1009, 1510 and 2010 ms. That's the same 500 ms rhythm as the uncompressed run on 16.4.4. Each chunk goes through gzip and out right away; nothing waits in a buffer.
I also ran an AI SDK 7.0.166 route (streamText(...).toUIMessageStreamResponse() with a mock model that emits a token every 500 ms). Same result: gzipped, and each text-delta showed up on its own about 500 ms apart.
I only tested next start directly. A reverse proxy or CDN in front of your app can still buffer, and I didn't test Vercel's platform.
Content-Length is still gone
This part comes from 16.4.5, not 16.4.6. The security release strips Content-Length, Content-Encoding and Transfer-Encoding from every route handler Response (the reasoning is in advisory GHSA-pc58-hwjq-p385, about forwarded fetch() responses). Issue #101120 reported that this breaks downloads and range requests, since pdf.js and download progress bars need the size.
That issue was closed by the compression PR. On 16.4.6, though, my 600 KB PDF still had no Content-Length on GET or HEAD. application/pdf isn't something the middleware compresses, so it just goes out chunked. If you serve files from a route handler and rely on that header, 16.4.6 doesn't bring it back.
Pre-compressed bodies break
Some handlers gzip their own output (a cached blob, a file from storage) and set Content-Encoding: gzip. On 16.4.4 that worked. On 16.4.5 Next drops the header, so fetch() hands your code raw gzip bytes and JSON.parse fails. On 16.4.6 it's worse in a different way. The header gets dropped, then the middleware gzips the already-gzipped body. The client decodes once and still has gzip bytes.
My test body was only 47 bytes, so the 1 KB threshold doesn't save you here. Route handler responses stream without a Content-Length, and the PR's own tests note that the threshold "never engages" for them.
Setting compress: false in next.config stops the double gzip, but you're back to the 16.4.5 behavior: the bytes arrive with no Content-Encoding header.
The fix that worked in my test: return the uncompressed body and let the server compress it. That's also what the PR description recommends ("the responses from route handlers should be returned uncompressed and be compressed by the outgoing server"). There's an open PR, #101452, to keep Content-Encoding on handler responses, but it isn't merged as of this morning.
What I'd check before upgrading
- Grep your route handlers for
content-encodingandgzipSync/CompressionStream. Those are the ones that break. - If you serve downloads from a route handler, check whether anything needs
Content-Length. It's been missing since 16.4.5. - If nginx or a CDN was compressing your API responses, it now receives responses that already say
Content-Encoding: gzip. I didn't test a proxy setup, so check the headers your users actually get.
The streaming part was the one I expected to bite, and it was fine. The quiet one is the handler that gzips its own body, because it fails in the client, not on the server.
Got a route handler that sets its own Content-Encoding? I'd like to know what it's serving.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.