HTTP Range requests, 206 and 416: how resuming a download works

How Range, Content-Range and If-Range work with 206 Partial Content and 416 Range Not Satisfiable, per RFC 9110, and why a server may ignore Range and send 200.

DevelopmentWebNetworking

Say a 10,000-byte download drops after 6,000 bytes. An HTTP Range request lets the client ask for only the remaining 4,000 bytes instead of starting over. The rules are in section 14 of RFC 9110, the HTTP semantics specification. Range requests are optional: a server may ignore the Range header and send the whole thing as for a normal GET. GET is also the only method for which the spec defines range handling.

Writing a byte range

Range: bytes=0-499 is the first 500 bytes. Byte positions start at zero and both ends are inclusive. Leave the end off, as in bytes=6000-, and you get everything from byte 6000 to the end; an end value at or beyond the actual length is read the same way. Leave the start off, as in bytes=-500, and you get the last 500 bytes, or the whole file if it is shorter than that. If the response has a content coding such as gzip, ranges count the encoded bytes, not the bytes you get after decoding.

The 206 response and Content-Range

When the server honours the range, it answers 206 Partial Content.

HTTP/1.1 206 Partial Content
Content-Range: bytes 6000-9999/10000
Content-Length: 4000

Content-Range has the form first-last/complete length, with /* when the complete length is unknown. Content-Length is the length of this message’s body, not of the whole file. The server may send only part of what was asked for, so the client has to read Content-Range to see what it actually received.

If several ranges are requested, as in bytes=0-99,5000-5099, and the server transfers more than one part, the body uses multipart/byteranges. Each part carries its own Content-Range; there is no top-level Content-Range. The server may merge overlapping or nearby ranges. It should send multiple parts in request order, but coalesced and unsatisfiable ranges can be omitted, so the response need not correspond one-for-one with the requested list. A server must not use multipart when the request asked for only one range.

416, and 200 instead

If none of the requested ranges can be satisfied, the answer is 416 Range Not Satisfiable. A byte range is satisfiable if its start is less than the current length, or if it is a suffix range of non-zero length. Asking for bytes=10000- on a 10,000-byte file can’t be satisfied, and the server is advised to report the current length with Content-Range: bytes */10000. A request for too many small ranges or for overlapping ranges can also be refused with 416; the spec treats such requests as signs of a broken client or a denial-of-service attack.

The spec itself notes, though, that many implementations send the full content with 200 in these cases, and that clients can’t count on getting a 416. Server settings differ too. nginx’s max_ranges has no limit by default, treats a request over the limit as if it had no ranges, and turns range support off at 0. Apache HTTP Server’s MaxRanges defaults to 200 and sends the complete resource when the limit is exceeded. A client sending Range should be ready for 206, 200 and 416.

If-Range: when the file changed in between

If the file on the server was replaced with a new version after the first 6,000 bytes arrived, stitching the pieces together gives you neither version. The spec allows pieces to be combined only when they share the same strong validator. If-Range is the header that does this check and the re-request in one go.

GET /file.zip HTTP/1.1
Range: bytes=6000-
If-Range: "v1"

A 10,000-byte file with bytes 0–5999 already received and 6000–9999 requested with Range. If the If-Range ETag matches and the server processes Range, the reply is 206 with Content-Range: bytes 6000-9999/10000; if not, Range is ignored and the whole new file normally arrives with 200.

If the ETag is still "v1" and the server processes Range, the remainder arrives with 206. If it differs, the server must ignore Range. For a GET that would otherwise produce 200, that means sending the complete new representation with 200. The ETag is matched with strong comparison, and a weak ETag (starting with W/) may not be used in If-Range. A Last-Modified date can be used only when there is no ETag and the date is a strong validator, and it must match exactly.

Accept-Ranges: bytes is only advice. Receiving it doesn’t mean the next Range request will come back as 206. A cache may store a 206 response as an incomplete 200 response, but must mark any partial response it sends to a client as 206 (RFC 9111). ETag and 304 are covered in no-cache vs no-store.

Sources

Links and content checked October 5, 2026.

Back to the blog