Skip to content

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.

Facts about this status code
Class2xx, Success
Defined inRFC 9110 §15.3.7
Cacheable by defaultYes, heuristically cacheable; caches can store the part and combine ranges of the same representation
Safe to retryYes; request the remaining ranges, with If-Range to detect a changed file
Relevant headers
  • Content-Range: required for a single part: which bytes are enclosed and the full length, e.g. bytes 0-1023/146515
  • Accept-Ranges: bytes advertises that the server honors Range requests
  • If-Range: request header; resume only if the ETag or date still matches, otherwise get a full 200

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 (Node.js)
// 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' });
});
Next.js App Router route handler
// 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'
    }
  });
}
Go net/http
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)
})
Python FastAPI
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)
Nginx
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;
}
Shell
# 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.