HTTP status code · Success (2xx)
206 Partial Content
206 Partial Content means the server is returning only the byte range the client asked for with a Range header, not the whole file. You see it constantly when a browser streams video or audio and seeks, and when a download manager resumes an interrupted download.
| Class | 2xx, Success |
|---|---|
| Defined in | RFC 9110 §15.3.7 |
| Cacheable by default | Yes, heuristically cacheable; caches can store the part and combine ranges of the same representation |
| Safe to retry | Yes; request the remaining ranges, with If-Range to detect a changed file |
| Relevant headers |
|
What 206 means
The client sends Range: bytes=21010-47021 and the server replies 206 with Content-Range: bytes 21010-47021/47022, where the number after the slash is the full length. RFC 9110, section 15.3.7, requires Content-Range for a single part and says Content-Length then counts only the bytes in this message. Several ranges in one request come back as a multipart/byteranges body.
Resuming safely relies on If-Range. The client sends the ETag or Last-Modified it saw before; if the file has changed since, the server ignores Range and sends the whole new file with 200, so the client never stitches two versions together. A server that cannot satisfy the range at all answers 416.
A 206 is heuristically cacheable, and caches may combine stored ranges of the same representation. Servers advertise support with Accept-Ranges: bytes; a server that ignores Range simply answers 200 with everything, which is valid but makes seeking and resuming impossible.
When to use it
- Serving video, audio and large downloads; let the static file layer do it rather than parsing Range yourself.
- Serving files from object storage through your app: forward the Range header to the storage API and pass its 206 and Content-Range back.
Common causes
If you are visiting the site
- A resumed download starts over from zero: the server does not support ranges, or the file changed and If-Range forced a full 200.
If you run the server
- Video will not play or seek in Safari: Safari probes with Range: bytes=0-1 and gives up when the server answers 200 with the whole file.
- Seeking breaks behind a proxy or app route that streams files but drops the Range header or rewrites the status to 200.
- Content-Range and the real body length disagree (often after on-the-fly compression), and the player stalls or corrupts the frame.
How to fix it
If you are visiting the site
- Use a download manager or browser that supports resume, and restart the download if the file was updated on the server.
If you run the server
- Serve media through express.static, http.ServeContent, Starlette FileResponse or nginx, which all implement Range, If-Range and 416.
- Test with curl -s -D - -o /dev/null -H "Range: bytes=0-99" URL and expect 206 with Content-Range: bytes 0-99/total.
- Do not compress video or audio on the fly; they are already compressed and ranges must refer to the stored bytes.
How to send 206
// express.static and res.sendFile answer Range requests
// with 206 Partial Content (and 416 when the range is invalid)
app.use('/media', express.static('media'));
app.get('/downloads/:name', (req, res) => {
res.sendFile(req.params.name, { root: 'downloads' });
});// app/media/[name]/route.ts (single range only)
import { open, stat } from 'node:fs/promises';
export async function GET(request: Request) {
const path = 'media/intro.mp4';
const { size } = await stat(path);
const m = /^bytes=(\d+)-(\d*)$/.exec(request.headers.get('range') ?? '');
if (!m) return new Response(null, { status: 200, headers: { 'Accept-Ranges': 'bytes' } }); // stream the full file here
const start = Number(m[1]);
const end = m[2] ? Math.min(Number(m[2]), size - 1) : size - 1;
if (start > end) {
return new Response(null, { status: 416, headers: { 'Content-Range': `bytes */${size}` } });
}
const file = await open(path);
const chunk = Buffer.alloc(end - start + 1);
await file.read(chunk, 0, chunk.length, start);
await file.close();
return new Response(chunk, {
status: 206,
headers: {
'Content-Range': `bytes ${start}-${end}/${size}`,
'Accept-Ranges': 'bytes',
'Content-Type': 'video/mp4'
}
});
}mux.HandleFunc("GET /media/{name}", func(w http.ResponseWriter, r *http.Request) {
f, err := os.Open(filepath.Join("media", filepath.Base(r.PathValue("name"))))
if err != nil {
http.NotFound(w, r)
return
}
defer f.Close()
info, _ := f.Stat()
// Handles Range and If-Range: 206 (StatusPartialContent), 416 or 200
http.ServeContent(w, r, info.Name(), info.ModTime(), f)
})from pathlib import Path
from fastapi import FastAPI
from fastapi.responses import FileResponse
app = FastAPI()
@app.get("/media/{name}")
def get_media(name: str):
# Current Starlette answers Range requests on FileResponse with 206
return FileResponse(Path("media") / Path(name).name)location /media/ {
root /srv;
# Static files get Range support (206) out of the box.
# Multi-range requests are then answered with the full file:
max_ranges 1;
}# Ask for the first 100 bytes and print only the headers
curl -s -D - -o /dev/null -H "Range: bytes=0-99" https://example.com/media/intro.mp4
# Expect: HTTP/2 206 and content-range: bytes 0-99/<total>Commonly confused with
- 206 vs 200
- A 200 to a Range request means the server ignored the range and sent the whole file, which is legal but breaks seeking and resume.
- 206 vs 416
- 416 Range Not Satisfiable means none of the requested range exists, usually because it starts past the end of the file.
Frequently asked questions
- Why do video requests show 206 in DevTools?
- Media elements fetch video in ranges so they can start playing quickly and jump to any point. Each 206 row is one chunk of the same file, which is normal.
- How do I check whether a server supports range requests?
- Send curl -I and look for Accept-Ranges: bytes, then request a small range with -H "Range: bytes=0-99". A 206 with Content-Range confirms support; a 200 with the full length means ranges are ignored.
- What happens if the file changes while a download is paused?
- If the client resumes with If-Range carrying the old ETag or date, the server sees the mismatch and sends the complete new file with 200 instead of a 206, so you never get a mix of two versions.
- Is a 206 response cacheable?
- Yes. RFC 9110 makes 206 heuristically cacheable unless explicit cache controls say otherwise, and a cache can combine stored ranges of the same representation.
Last reviewed by Arielton Oberek.