< HTTP/1.1 206 Partial Content206 Partial Content: Range Requests Explained
206 Partial Content returns only the byte range requested. See a curl transcript, Content-Range, If-Range, video seeking and resumable downloads.
- Cacheable
- Yes, by default
- Client action
- Stitch the ranges together
- Usually sent by
- Origin or CDN (Range requests)
- Spec
- RFC 9110 §15.3.7
- Often confused with
- 200
On this page
TL;DR: 206 Partial Content is the answer to a request with a
Rangeheader: the body is just those bytes, andContent-Rangesays where they sit in the full resource. It powers video seeking and resumable downloads, and a server that ignoresRangereplies 200 instead.
When you will see it
A client sends Range: bytes=0-99 and the server supports it. You can watch this with curl:
curl -s -D - -o /dev/null -r 0-99 https://example.com/files/report.pdf
HTTP/2 206
content-type: application/pdf
content-length: 100
content-range: bytes 0-99/2457600
accept-ranges: bytes
etag: "5f1c-6032a8b4c1a00"
last-modified: Tue, 15 Sep 2026 08:12:41 GMT
Note what changed relative to a 200. content-length is 100, the length of this body, not the file. content-range: bytes 0-99/2457600 reads “bytes 0 through 99 inclusive, of 2,457,600 total”. Both ends of a range are inclusive, so 0-99 is 100 bytes, a classic off-by-one when building these by hand.
A server that does not support ranges ignores Range and returns 200 with the full body, usually with Accept-Ranges: none or no Accept-Ranges at all. That is valid per RFC 9110, so clients must check the status code, not assume.
Where it matters
Video and audio seeking. The browser’s media stack requests the start of the file (often bytes=0- or a small probe), reads the container header, then issues new range requests as you scrub. A video behind a server that cannot do ranges plays from the start but cannot seek. Safari is the strict one: it sends Range: bytes=0-1 first and refuses to play if the answer is not a correct 206 with Content-Range. A video that works in Chrome and stays blank in Safari is almost always a range problem, commonly introduced by a proxy or Node handler that streams the file with res.send or pipe and no range logic.
Resumable downloads. curl -C - and wget -c work out how many bytes exist locally and send Range: bytes=<n>-. The server answers 206 with the remainder.
Large file parallel fetches. Download accelerators and tools like aria2 split a file into ranges and fetch them in parallel. Object stores (S3, GCS) serve ranges natively, which is how Parquet readers pull only the footer and the needed column chunks.
PDF viewers. Browsers’ PDF viewers fetch the trailer and cross-reference table from the end of the file with a suffix range (Range: bytes=-1024) to render page one before the rest has downloaded (“fast web view”).
Range forms
| Header | Meaning |
|---|---|
Range: bytes=0-99 | First 100 bytes |
Range: bytes=500- | From byte 500 to the end |
Range: bytes=-500 | Last 500 bytes (suffix range) |
Range: bytes=0-99,200-299 | Two ranges; the response may be multipart/byteranges |
The only range unit defined by RFC 9110 is bytes. Ranges apply to GET requests; other methods must ignore Range.
Resuming safely with If-Range
If a file changes between the first attempt and the resume, a naive Range request gives you the tail of the new file glued to the head of the old one. If-Range fixes that:
GET /files/report.pdf HTTP/1.1
Host: example.com
Range: bytes=1048576-
If-Range: "5f1c-6032a8b4c1a00"
If the validator still matches, the server replies 206 with the rest. If it does not, the server replies 200 with the complete new file, and the client has to start over. Two gotchas: the validator must be a strong ETag (no W/ prefix) or a Last-Modified date that is exactly the one the server sent; and a client that sends If-Range must handle a 200 reply by discarding what it already has.
Multiple ranges
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=3d6b6a416f9b5
Content-Length: 385
--3d6b6a416f9b5
Content-Type: application/pdf
Content-Range: bytes 0-99/2457600
...100 bytes...
--3d6b6a416f9b5
Content-Type: application/pdf
Content-Range: bytes 200-299/2457600
...100 bytes...
--3d6b6a416f9b5--
The top-level response has no Content-Range; each part carries its own. Servers defend against abusive requests (hundreds of overlapping ranges was a real denial-of-service vector against Apache httpd in 2011), so expect them to reply 200 with the full body or 416 when a multi-range request looks pathological. nginx exposes max_ranges to cap this.
Server-side notes
- nginx serves ranges for static files by default. When proxying, ranges pass through to the upstream; to serve cached slices of a large upstream object, look at the
slicemodule (slice 1m;with$slice_rangein the cache key). - Express
res.sendFile()andexpress.statichandleRangeandIf-Rangethrough thesendpackage.res.send(buffer)and a manualfs.createReadStream().pipe(res)do not. - Compression and ranges. Byte ranges address the encoded representation. If a proxy compresses on the fly, offsets would shift between requests, which is why many servers disable compression for requests with
Range, or skip range support for dynamically compressed content. Content-Lengthmust be right. For 206 it is the length of the part, not the file. Reverse proxies that rewrite it are a common cause of truncated media.
A minimal Node handler that supports a single range, to show the mechanics:
import fs from 'node:fs'
import http from 'node:http'
http.createServer((req, res) => {
const path = './video.mp4'
const size = fs.statSync(path).size
const header = req.headers.range
res.setHeader('Accept-Ranges', 'bytes')
if (!header) {
res.writeHead(200, { 'Content-Length': size, 'Content-Type': 'video/mp4' })
return fs.createReadStream(path).pipe(res)
}
const m = /^bytes=(\d*)-(\d*)$/.exec(header)
if (!m || (m[1] === '' && m[2] === '')) {
res.writeHead(416, { 'Content-Range': `bytes */${size}` })
return res.end()
}
let start = m[1] === '' ? size - Number(m[2]) : Number(m[1])
let end = m[1] === '' || m[2] === '' ? size - 1 : Math.min(Number(m[2]), size - 1)
if (start >= size || start > end) {
res.writeHead(416, { 'Content-Range': `bytes */${size}` })
return res.end()
}
res.writeHead(206, {
'Content-Range': `bytes ${start}-${end}/${size}`,
'Content-Length': end - start + 1,
'Content-Type': 'video/mp4'
})
fs.createReadStream(path, { start, end }).pipe(res)
}).listen(8080)
Debugging checklist
curl -I https://host/fileand look forAccept-Ranges: bytes. Absence is not proof of no support, butnoneis.curl -s -D - -o /dev/null -r 0-1 https://host/file. You want206andContent-Range: bytes 0-1/<size>. A200means a layer is ignoring ranges.- Test through and around the CDN to find which layer strips it (compare
Via,Age,CF-Cache-Status,X-Cache). - Check
Content-Lengthon the 206 equals end - start + 1. - For resume problems, check that the ETag is stable across origin servers behind the load balancer; ETags that differ per node make
If-Rangefail on every other request. nginx and Apache 2.4 build file ETags from modification time and size, so nodes with different file mtimes after a deploy disagree; Apache 2.2’s default also included the inode.
Related
- 200 OK: what you get when the server ignores
Range. - 416 Range Not Satisfiable: the requested range is outside the resource.
- 304 Not Modified: the conditional-request sibling.
- Range, Content-Range, Accept-Ranges, If-Range, ETag
Frequently asked questions
What does 206 Partial Content mean?
The server understood a Range request and is returning only the requested byte range of the resource. The Content-Range header says which bytes are in the body and how large the full resource is.
Why does my video need 206 to play in Safari?
Safari starts media playback with a small range request such as Range: bytes=0-1 and expects a 206 with a valid Content-Range. A server that answers 200 with the whole file, or that does not support ranges, can leave the video element stuck even though Chrome plays it fine.
What is the difference between 206 and 200 for a Range request?
A server is allowed to ignore a Range header and reply 200 with the full body. A client that sent Range must therefore check the status: only 206 means the body is a slice. Treating a 200 body as a slice corrupts resumed downloads.
How does If-Range work?
If-Range carries a strong ETag or a Last-Modified date. If it still matches the current resource, the server returns 206 with the requested slice. If not, it returns 200 with the whole new resource, which prevents stitching bytes from two different versions of a file.
Can I send multiple ranges in one request?
Yes, as in Range: bytes=0-99,200-299. The server may reply with 206 and a multipart/byteranges body, each part carrying its own Content-Range, or it may collapse or refuse the request. Browsers rarely send multi-range requests, PDF viewers sometimes do.
Is a 206 response cacheable?
Yes, 206 is cacheable by default, and a cache can combine stored ranges of the same representation. In practice many CDNs fetch the whole object or fixed-size slices from the origin rather than relaying arbitrary client ranges.
Sources
Related
HTTP Range Requests: 206, Resume and Video Seeking
How HTTP range requests work: Range, Accept-Ranges, 206 and Content-Range, multipart/byteranges, 416, If-Range for safe resumes, and CDN and compression traps.
226 IM Used: HTTP Delta Encoding
226 IM Used is the response to a delta-encoded request under RFC 3229. See the A-IM and IM headers, how it works, and why almost nothing implements it.
HTTP 200 OK: What It Means and When to Use It
200 OK means the request succeeded and the response carries the result. What a 200 should contain, and when 201, 204 or 206 is the better answer for your API.
202 Accepted
The request was accepted for processing but not completed yet. Learn when to use 202 for asynchronous operations.