416 Range Not Satisfiable
Client error response defined by RFC 9110 §15.5.17.
Last updated July 27, 2026
Common causes at a glance
- A resumed download computing its start offset from a stale or truncated local file
- A media player seeking past the end of a file that was replaced with a shorter one
- Range applied to a dynamically generated response whose length differs between requests
- A byte-range request against a compressed representation where offsets refer to a different encoding
Reproduce it
curl -i -H 'Range: bytes=99999999-' https://cdn.example.com/video/clip.mp4What 416 Range Not Satisfiable means
A range request asks for a slice of a resource: Range:
bytes=1000-1999. The server answers 206 Partial Content with
that slice, or 416 if the slice does not exist. The rule that matters is that the first
byte position must be strictly less than the current length: asking for
bytes=5000- of a 5000-byte file is unsatisfiable, because valid offsets
stop at 4999.
There is an escape hatch. Servers may ignore Range entirely and return the
whole thing with 200, and many do, so a client cannot rely on getting 416 even when it
is the most appropriate answer. Resume logic has to handle both outcomes.
What the spec says
RFC 9110 §15.5.17 allows 416 both when no requested
range is satisfiable and when the client asked for an excessive number of small or
overlapping ranges, a recognised denial-of-service vector. A server generating 416 for a
byte-range request should include Content-Range in
the unsatisfied form, Content-Range: bytes */47022, so the client learns
the real length. The Range field is §14.2 and
If-Range exists so a resumed transfer against a
changed file falls back to a full fetch rather than stitching together mismatched
bytes.
What actually causes it
Resumed downloads produce most real 416s. The client records how many
bytes it already has, restarts with Range: bytes=N-, and gets rejected
because the remote file is now shorter than N: someone re-encoded the video, truncated
the log, or the partial file on disk was corrupted by an earlier failure. Streaming
players hit the same wall when a manifest points at a segment that has been rotated
out. The subtler variant involves compression: if a CDN serves a different
encoding than the one whose length the client measured, offsets are computed against
representations that were never the same size. Deleting the partial file and starting
over fixes more of these than any header tuning.
How to debug it
Get the authoritative length first, then reason about offsets:
curl -sI https://cdn.example.com/video/clip.mp4 | grep -iE '^(content-length|accept-ranges|etag)'
curl -i -H 'Range: bytes=99999999-' https://cdn.example.com/video/clip.mp4 | head -5
A 416 response should carry Content-Range: bytes */LENGTH; that number is
the truth, and it is usually smaller than whatever your client assumed. If
Accept-Ranges: none is present, or the header is absent, the origin does
not do ranges and you will get a 200 with the full body regardless. Compare the
ETag from the original download with the current one: if they differ the
file changed underneath you and resumption is unsafe, which is what
If-Range is for. Check edge and origin separately with the
header inspector, since a CDN may report a different length
during a cache fill.
Working examples
curl
# What is the actual length, and does the origin even do ranges?
curl -sI https://cdn.example.com/video/clip.mp4 \
| grep -iE '^(content-length|accept-ranges|etag)'
# A range past end-of-file. Read Content-Range in the 416 for the real size.
curl -i -H 'Range: bytes=99999999-' https://cdn.example.com/video/clip.mp4 | head -5
# Safe resume: only serve the range if the file has not changed.
curl -i -C 500000 -H 'If-Range: "abc123"' \
-o clip.mp4 https://cdn.example.com/video/clip.mp4
Python (requests)
import os
import requests
url = "https://cdn.example.com/video/clip.mp4"
path = "clip.mp4"
have = os.path.getsize(path) if os.path.exists(path) else 0
resp = requests.get(url, headers={"Range": f"bytes={have}-"}, stream=True, timeout=60)
if resp.status_code == 416:
# Content-Range: bytes */47022 tells you the real length.
print("unsatisfiable;", resp.headers.get("Content-Range"))
os.remove(path) # local file is longer than the remote one
elif resp.status_code == 206:
with open(path, "ab") as fh:
for chunk in resp.iter_content(65536):
fh.write(chunk)
else:
print("server ignored Range:", resp.status_code)
Node (fetch)
import { stat } from "node:fs/promises";
const url = "https://cdn.example.com/video/clip.mp4";
const have = await stat("clip.mp4").then((s) => s.size, () => 0);
const res = await fetch(url, { headers: { Range: `bytes=${have}-` } });
if (res.status === 416) {
console.log("unsatisfiable;", res.headers.get("content-range"));
} else if (res.status === 206) {
console.log("resuming", res.headers.get("content-range"));
} else {
console.log("Range ignored, full body:", res.status);
}Reference and tooling
The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.
Other 4xx codes
- 400 Bad Request
- 401 Unauthorized
- 402 Payment Required
- 403 Forbidden
- 404 Not Found
- 405 Method Not Allowed
- 406 Not Acceptable
- 407 Proxy Authentication Required
- 408 Request Timeout
- 409 Conflict
- 410 Gone
- 411 Length Required
- 412 Precondition Failed
- 413 Content Too Large
- 414 URI Too Long
- 415 Unsupported Media Type
- 417 Expectation Failed
- 418 (Unused)
- 419 Page Expired
- 421 Misdirected Request
- 422 Unprocessable Content
- 423 Locked
- 424 Failed Dependency
- 425 Too Early
- 426 Upgrade Required
- 428 Precondition Required
- 429 Too Many Requests
- 431 Request Header Fields Too Large
- 444 No Response
- 451 Unavailable For Legal Reasons
- 494 Request Header Too Large
- 495 SSL Certificate Error
- 496 SSL Certificate Required
- 497 HTTP Request Sent to HTTPS Port
- 499 Client Closed Request