Range header and 206 Partial Content (tested with nginx and curl)
A Range request asks for part of a file. If the server supports it, it answers 206 Partial Content with a Content-Range header saying which bytes you got. We sent ten different Range headers to nginx and wrote down the exact response.
1. The results
File: 10,000 bytes, served by nginx 1.31.6 with its default static-file handling. Command: curl -sD- -o out -H "Range: ..." http://127.0.0.1:8957/f.bin.
| Request header | Status | Content-Range | Content-Length |
|---|---|---|---|
| (no Range) | 200 | (none); Accept-Ranges: bytes present | 10000 |
Range: bytes=0-99 | 206 | bytes 0-99/10000 | 100 |
Range: bytes=-100 (last 100) | 206 | bytes 9900-9999/10000 | 100 |
Range: bytes=9990- | 206 | bytes 9990-9999/10000 | 10 |
Range: bytes=0-0 (first byte) | 206 | bytes 0-0/10000 | 1 |
Range: bytes=9990-20000 (end past EOF) | 206 | bytes 9990-9999/10000 | 10 |
Range: bytes=0-9,20-29 | 206, multipart/byteranges | (per part) | 250 |
Range: bytes=20000- (start past EOF) | 416 | bytes */10000 | 197 (HTML error page) |
Range: bytes=abc (malformed) | 416 | bytes */10000 | 197 (HTML error page) |
Byte positions are zero-based and inclusive: 0-99 is 100 bytes. An end past the file was clipped to the last byte rather than rejected; only a start past the end gave 416. The 416 carried Content-Range: bytes */10000, which tells the client the full size.
2. Malformed ranges: 416, not ignored
We expected nginx to ignore bytes=abc and send the whole file with 200. It did not: nginx 1.31.6 answered 416 Requested Range Not Satisfiable. If a client builds Range headers from user input, validate them, because a bad value costs you the response rather than falling back to the full file. Other servers may behave differently; we only tested nginx.
3. Multiple ranges
Range: bytes=0-9,20-29 returned one 206 with Content-Type: multipart/byteranges; boundary=00000000000000000001; the next multi-range request got ...0002, so the boundary is a counter, not random. Each part has its own headers:
--00000000000000000002
Content-Type: application/octet-stream
Content-Range: bytes 0-9/10000
(10 bytes)
--00000000000000000002
Content-Type: application/octet-stream
Content-Range: bytes 20-29/10000
(10 bytes)
--00000000000000000002--
20 bytes of data became a 250-byte body. The top-level response had no Content-Range header; it is only inside the parts.
4. If-Range: resume only if the file has not changed
| Headers sent | Result |
|---|---|
Range: bytes=0-99 + If-Range: "6abecfd6-2710" (current ETag) | 206, 100 bytes |
Range: bytes=0-99 + If-Range: "deadbeef" (stale ETag) | 200, full 10,000 bytes |
This is the safe way to resume: if the file changed, you get the whole new file instead of a mix of old and new bytes. A stale If-Range is not an error; the Range is just dropped. nginx builds the ETag from the modification time and size (how nginx ETags work).
5. gzip and Range do not mix (in nginx)
On a 10,000-byte text file with gzip on, a request with Accept-Encoding: gzip and Range: bytes=0-99 got 200 OK with the full gzipped body (119 bytes), Content-Encoding: gzip, a weak ETag (W/"..."), no Content-Length and no Accept-Ranges header. nginx ignored the Range entirely. Without Accept-Encoding the same text file was served uncompressed and a two-range request on it returned 206 as in section 3. For large downloads, video and anything resumable, do not compress on the fly. The gzipped response came with Transfer-Encoding: chunked instead of a Content-Length: see chunked vs Content-Length.
6. curl shortcuts
curl -r 0-99 URLsent the same request as-H "Range: bytes=0-99"and got the identical 206 withContent-Range: bytes 0-99/10000.curl -C - -o file URLwith a 4,000-byte partial file on disk asked for the rest and got 206 withContent-Range: bytes 4000-9999/10000; the finished file matched the original byte for byte.
7. Things we did not test
Doc-based, not measured here: nginx's max_ranges directive limits how many ranges per request it honours (0 disables range support); proxied responses only support Range if the upstream does, or if nginx serves them from cache. Per RFC 9110, a server may also ignore Range and send 200, so clients must handle both.
See the response headers of any URL, including Accept-Ranges, with curl -sI or the free HTTP headers tool.
Frequently asked questions
What does HTTP 206 Partial Content mean?
The server is sending only the byte range you asked for in the Range header. In our nginx test, Range: bytes=0-99 on a 10,000-byte file returned 206 with Content-Range: bytes 0-99/10000 and Content-Length: 100.
How do I know if a server supports Range requests?
Look for Accept-Ranges: bytes on a normal 200 response, then send a Range request and check for 206. nginx sent Accept-Ranges: bytes for static files, but not on gzipped responses, where it ignored Range and returned 200.
Why do I get 416 Range Not Satisfiable?
The range start is past the end of the file, or the header is malformed. nginx 1.31.6 returned 416 with Content-Range: bytes */10000 for both bytes=20000- and bytes=abc. An end past EOF (bytes=9990-20000) was clipped, not rejected.
What is Range: bytes=-100?
A suffix range: the last 100 bytes. nginx answered Content-Range: bytes 9900-9999/10000.
What does If-Range do?
It makes the Range conditional. With the current ETag nginx returned 206; with a stale ETag it returned 200 and the full file, so a resumed download never mixes old and new versions.
Written with AI assistance. Measured on 2026-10-01 with curl 8.14.1 (curl -sD-) against a throwaway nginx 1.31.6 (nginx:alpine) container serving a 10,000-byte static file, plus a 10,000-byte text file with gzip on (gzip_types text/plain). Default nginx config otherwise; container removed afterwards. PlainHeaders is our extension; the other methods are free.