Transfer-Encoding: chunked vs Content-Length: when servers use which (tested in Node and nginx)
An HTTP/1.1 body needs a known end: either a Content-Length up front, or chunked encoding where each piece carries its own size. We logged which one Node and nginx picked.
1. Which header did the response get?
| Server / case | Response framing |
|---|---|
Node res.end(body) in one call (12 B and 5,000 B) | Content-Length |
Node res.write() then res.end() | Transfer-Encoding: chunked |
| nginx static file | Content-Length: 17 |
nginx static file, gzip on, client sends Accept-Encoding: gzip | chunked + Content-Encoding: gzip (compressed size isn't known in advance) |
nginx proxy_pass | passed through: upstream Content-Length stayed Content-Length, upstream chunked stayed chunked |
| nginx, client speaking HTTP/1.0 | neither header: body ends when the connection closes |
So "why does my response have no Content-Length?" usually means compression or streaming. Both are fine; clients handle either.
2. What chunks look like
$ curl --raw http://localhost/write # CR/LF shown as \r\n
6\r\n
part1\n\r\n
6\r\n
part2\n\r\n
0\r\n
\r\n
Each chunk is its size in hex, CRLF, the data, CRLF; a zero-size chunk ends the body. Plain curl (without --raw) strips this and shows only part1/part2. HTTP/2 and HTTP/3 have no chunked encoding at all; framing is built into the protocol.
3. Requests, and never sending both
- A chunked request body through nginx reached the upstream as
Content-Length: 6: nginx buffered it and re-framed it. - A request with both
Content-LengthandTransfer-Encoding: chunkedgot 400 Bad Request from nginx and from Node. That ambiguity is the basis of request smuggling, so modern servers refuse it. - Node will still send both on a response if you set both headers yourself: we got
Content-Length: 12andTransfer-Encoding: chunkedwith a chunk-framed body. Set neither by hand; let the server choose.
Check what a live URL sends with the free HTTP headers checker; for compression choices see gzip vs br in nginx, measured.
Frequently asked questions
What is the difference between Transfer-Encoding chunked and Content-Length?
Content-Length gives the body size up front; chunked sends the body in pieces, each prefixed with its size, ending with a 0-size chunk. In our tests Node used Content-Length for res.end(body) and chunked for res.write() streaming.
Why does nginx send Transfer-Encoding: chunked instead of Content-Length?
In our test a static file had Content-Length until gzip compressed it; the gzip response was chunked because the compressed size isn't known in advance. Proxied responses kept the upstream's framing.
Can a request have both Content-Length and Transfer-Encoding?
It shouldn't: nginx 1.31 and Node 24 both answered 400 Bad Request to one that did.
Does HTTP/2 use chunked encoding?
No. HTTP/2 and HTTP/3 frame data themselves; Transfer-Encoding: chunked is HTTP/1.1 only.
How do I see the raw chunks?
curl --raw URL shows the hex sizes and CRLFs; plain curl decodes them.
Written with AI assistance. Measured on 2026-10-01 with Node.js 24 (http module) and a throwaway nginx 1.31.6 container (static files with gzip on, and proxy_pass to the Node server; removed afterwards), using curl -D and --raw and raw requests over nc. The HTTP/2 remark is from RFC 9113, not this test. PlainHeaders is our extension; the other methods are free.