Skip to content
Control Plane Labs

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.mp4

What 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