Request and response header
< Transfer-Encoding: chunkedTransfer-Encoding Header
Learn how the Transfer-Encoding header specifies encoding formats like chunked transfer for streaming responses when content length is unknown beforehand.
- Direction
- Request and response
- Category
- Connection management
- Spec
- RFC 9112 §6.1
- JS can set it
- No: forbidden header name
- Hop-by-hop
- Yes: not forwarded by proxies
- Note
- HTTP/1.1 only; not used in HTTP/2 or HTTP/3
On this page
TL;DR:
Transfer-Encoding: chunkedlets an HTTP/1.1 server start sending a body before it knows the total size. The body goes out in chunks, each prefixed with its length in hexadecimal, and a zero-length chunk marks the end. Your HTTP library writes this framing for you. HTTP/2 and HTTP/3 have their own framing and don’t use this header at all.
What is Transfer-Encoding?
A transfer coding applies to a single hop of the message, not to the resource itself. A proxy can decode the chunks it receives and frame the body differently on the next hop. Content-Encoding, such as gzip, is the other kind: it describes how the representation itself is encoded.
How Transfer-Encoding Works
Here’s an HTTP/1.1 response (wire examples on this page are trimmed), with each \r\n written out. Each displayed line is one run of bytes; the line breaks you see aren’t on the wire:
HTTP/1.1 200 OK\r\n
Content-Type: text/plain\r\n
Transfer-Encoding: chunked\r\n
\r\n
5\r\n
Hello\r\n
6\r\n
World\r\n
0\r\n
\r\n
The decoded body is Hello World, eleven bytes. Each chunk size counts only the data, not the CRLFs around it. The grammar is in RFC 9112 §7.1.
Syntax
Transfer-Encoding: chunked
Transfer-Encoding: gzip, chunked
Codings apply in the order listed, so the second line means gzip first, then chunked. Chunked can only appear once. In a request, chunked has to be the last coding. In a response, if the last coding isn’t chunked, the body ends only when the connection closes, which is why chunked, gzip can’t be used on a connection you want to reuse. See RFC 9112 §6.1.
Common Examples
For everyday compressed streaming over HTTP/1.1, the compression and the framing sit in separate headers:
Content-Encoding: gzip
Transfer-Encoding: chunked
A response without Transfer-Encoding isn’t necessarily sent in one go. A server that knows the length up front can still write the body a piece at a time.
Real-World Scenarios
Over HTTP/1.1, chunking lets a server send bytes as soon as it has them. Server-sent events have their own event format on top of that, and SSE events and HTTP chunks don’t have to line up.
At the application layer, one event looks like this:
data: {"status":"ready"}
Here \n stands for a line feed. The blank line ends the SSE event; it has nothing to do with HTTP’s zero chunk. One event can span several chunks, and one chunk can hold several events. The browser’s EventSource parses SSE for you; if you read the stream yourself, you have to split events yourself too.
EventSource reconnects automatically when a stream closes. If you point it at the finite demo below, call source.close() when you’re done, or it will open another connection. The stream reader example further down reads the response once.
Server Implementation
This complete Node HTTP/1.1 server leaves the framing to the HTTP stack. Write plain content and let Node add the hex size lines:
const http = require('node:http')
http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
res.write('Hello')
res.end(' World')
}).listen(3000)
Node’s HTTP stack generates the message framing. Two write() calls won’t necessarily become two packets or arrive at two different moments.
An SSE demo that stops after a few events makes incremental delivery easier to watch. Save it as its own Node script and run it in place of the server above:
const http = require('node:http')
http.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-store'
})
res.flushHeaders()
let count = 0
const timer = setInterval(() => {
res.write(`data: ${JSON.stringify({ count: ++count })}\n\n`)
if (count === 3) {
clearInterval(timer)
res.end()
}
}, 1000)
res.on('close', () => clearInterval(timer))
}).listen(3000)
It sends three events, one per second. That interval is the demo’s choice; chunked encoding doesn’t set any pace. Clearing the timer on close stops the work when the client disconnects. flushHeaders() gets the headers out before the first event, but a proxy further down the line can still buffer.
To proxy this server through nginx, add this to a server block:
location /events {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
}
proxy_buffering is on by default. Turning it off makes nginx forward upstream data as it arrives, and proxy_cache off cancels any caching inherited from a parent block. The app can also opt out per response with X-Accel-Buffering: no, unless proxy_ignore_headers is set to ignore it. proxy_http_version only affects the nginx-to-upstream hop; the browser side negotiates its own protocol.
In Django, StreamingHttpResponse streams an iterator, such as CSV rows. This synchronous view is for WSGI:
from django.http import StreamingHttpResponse
def export(request):
def rows():
yield 'status\n'
yield 'ready\n'
return StreamingHttpResponse(rows(), content_type='text/csv')
Your code yields content, and the server’s HTTP stack picks the wire framing. Under ASGI, use an async iterator instead. Django documents that if it has to adapt the wrong kind of iterator, it consumes the whole thing first, so nothing streams.
Getting Transfer-Encoding right
Never send Content-Length and Transfer-Encoding on the same message. That combination is a classic sign of request smuggling. RFC 9112 §6.3 defines how to handle it, and requires an intermediary that forwards such a message to drop the received Content-Length and process the transfer coding first.
HTTP/2 forbids this connection-specific header (RFC 9113 §8.2.2), and so does HTTP/3 (RFC 9114 §4.1). A proxy can receive chunked HTTP/1.1 on one side and send HTTP/2 DATA frames on the other.
In Node, correct framing doesn’t take care of backpressure for you. When res.write() returns false, data is piling up in memory; wait for the drain event before you write more. A fast export loop that ignores this can build up a huge backlog for a slow client, with every chunk perfectly framed.
Chunked Encoding Format
After the zero-size last chunk comes an optional trailer section, ended by an empty line. With no trailers, the body ends in 0\r\n\r\n. Sizes are hexadecimal: A means ten bytes of data, and 10 would mean sixteen.
If the sender stops after 5\r\nHello\r\n and never sends the last chunk, the message is incomplete, even though five valid bytes arrived. For a chunked message, the connection closing doesn’t count as an ending. That’s what bites you when a process crashes halfway through an export: the client has some bytes, but the response never completed.
An empty chunk can’t work as a heartbeat, because a zero-size chunk ends the body. Send heartbeats as content, such as an SSE comment.
Common Use Cases
Chunking lets a generated response start before its total size is known. It only provides the framing, though; the app or a proxy can still buffer the stream. Debug framing and buffering as two separate problems.
In the browser, fetch() hands you the decoded bytes as a stream. For the finite text stream above:
const response = await fetch('/events')
if (!response.ok) throw new Error(`HTTP ${response.status}`)
if (!response.body) throw new Error('No response body')
const reader = response.body.getReader()
const decoder = new TextDecoder()
try {
while (true) {
const { value, done } = await reader.read()
if (done) break
console.log(decoder.decode(value, { stream: true }))
}
console.log(decoder.decode())
} finally {
reader.releaseLock()
}
getReader() locks the stream while you read it. decode(..., { stream: true }) holds on to a partial multi-byte character until the next read. Each logged string can cut through the middle of a line or an SSE event. If you want whole events as objects, use EventSource instead.
Testing Transfer-Encoding
Against the local Node example:
curl --http1.1 --raw -i http://localhost:3000/
--raw turns off curl’s transfer decoding so you can see the chunk framing; without it, curl strips the size lines. Browser Fetch only ever gives you the decoded body, never the raw HTTP/1.1 chunk sizes.
To watch the app’s delivery timing without the chunk markers:
curl --http1.1 -N http://localhost:3000/events
curl --http1.1 -N https://example.com/events
-N turns off curl’s output buffering. Compare a direct request to the upstream with one through the public route. If events trickle in directly but all arrive at once through the public route, check proxy buffering and compression before you touch the app’s writes. Check the negotiated protocol too: on HTTP/2 there’s no Transfer-Encoding header, which is normal and has nothing to do with a delayed stream.
Related Headers
Frequently asked questions
What is Transfer-Encoding?
Transfer-Encoding specifies how the message body is encoded for transfer. The most common value is "chunked" which sends data in pieces without knowing the total size upfront.
What is chunked transfer encoding?
Chunked encoding sends the body in pieces, each prefixed with its size. It allows streaming responses when the total size is unknown. The transfer ends with a zero-length chunk.
When should I use chunked encoding?
Use chunked when you do not know the response size upfront, for streaming data, or for server-sent events. It allows sending data as it becomes available.
What is the difference between Transfer-Encoding and Content-Encoding?
Transfer-Encoding is about how data is transferred (chunked). Content-Encoding is about compression (gzip). Transfer-Encoding is hop-by-hop; Content-Encoding is end-to-end.
Sources
Related
Content-Disposition Header
Learn how the Content-Disposition header controls whether content displays inline or downloads as an attachment. Set custom filenames for file downloads.
Content-Language Header
Learn how the Content-Language header specifies the natural language(s) of response content. Understand language tags and internationalization best practices.
Content-Length
Learn how Content-Length specifies the body size in bytes. Essential for progress indicators, connection management, and chunked transfer decisions.
Content-Location Header
Learn how Content-Location indicates an alternate URL for returned content. Useful for content negotiation and identifying canonical resource locations.