What Are Range Requests?
Range requests allow a client to request only a portion of a resource instead of the whole thing. This is the mechanism behind:
- Resumable downloads — resume after a dropped connection
- Video streaming — seek to any point in a video without re-downloading from the start
- Parallel downloads — download different parts of a large file simultaneously
Range Header Syntax
The Range header uses byte ranges (zero-indexed):
Range: bytes=0-1023 # First 1,024 bytes
Range: bytes=500-999 # Bytes 500 through 999 (inclusive)
Range: bytes=-500 # Last 500 bytes
Range: bytes=9500- # From byte 9,500 to end of file
Before making a range request, clients often send a HEAD request to check if the server supports ranges:
curl -I https://example.com/large-file.zip
# Look for: Accept-Ranges: bytes
If the server responds with Accept-Ranges: none, it does not support range requests.
206 Partial Content
When a valid range request is honored, the server returns 206 Partial Content with Content-Range telling the client what slice it received:
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1023/146515
Content-Length: 1024
Accept-Ranges: bytes
Content-Type: application/zip
The Content-Range format is bytes <start>-<end>/<total>. If the total is unknown, * is used instead.
Multipart Range Responses
A client can request multiple non-contiguous ranges in a single request:
Range: bytes=0-50, 100-150
The server responds with a multipart body:
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=BOUNDARY
--BOUNDARY
Content-Type: application/zip
Content-Range: bytes 0-50/146515
<bytes 0-50>
--BOUNDARY
Content-Type: application/zip
Content-Range: bytes 100-150/146515
<bytes 100-150>
--BOUNDARY--
Multipart ranges are rarely used in practice — the overhead of multiple requests is usually lower than parsing a multipart body.
Resumable Downloads
Here is how a client implements a resumable download:
import os
import requests
def download_resumable(url: str, dest: str) -> None:
existing_size = os.path.getsize(dest) if os.path.exists(dest) else 0
headers = {}
if existing_size > 0:
headers['Range'] = f'bytes={existing_size}-'
response = requests.get(url, headers=headers, stream=True)
mode = 'ab' if response.status_code == 206 else 'wb'
with open(dest, mode) as f:
for chunk in response.iter_content(chunk_size=65536):
f.write(chunk)
Video Streaming
Browsers use range requests extensively for <video> elements. When a user seeks to a timestamp, the browser sends a range request for the byte offset corresponding to that timestamp:
Range: bytes=4194304-
This is why you must serve video files from a server that supports Accept-Ranges: bytes. Static file servers (nginx, S3, CDNs) handle this automatically.
nginx configuration for range support:
location /videos/ {
root /var/www/media;
# nginx supports range requests by default for static files
}
416 Range Not Satisfiable
When the requested range is invalid (e.g., start > total size), the server returns 416 Range Not Satisfiable:
HTTP/1.1 416 Range Not Satisfiable
Content-Range: bytes */146515
The */<total> format in Content-Range tells the client the actual file size so it can adjust its request.
Conditional Range Requests
To avoid downloading the wrong data if the file changed between requests, combine Range with If-Range:
Range: bytes=500-999
If-Range: "abc123etag"
If the ETag still matches, the server returns 206 with the range. If the resource changed, it returns 200 with the full resource instead of the range — preventing corrupted partial content.
Summary
Range requests are essential infrastructure for video streaming and large file downloads. Key points:
- Check
Accept-Ranges: bytesbefore attempting a range request - A successful range response uses 206 Partial Content
- Use
If-Rangewith an ETag for safe resumable downloads - Handle 416 by re-querying the file size from
Content-Range: */N