Learning objectives
After reading this article you will be able to:
- Understand how Range, 206 and Content-Range make seeking possible
- Test with curl whether your storage or server accepts range requests
- Fix the common errors: moov at the end of an MP4, 200 instead of 206 and caching
On this page
- What is a range request?
- Why do video and audio depend on it?
- How do I test whether the server accepts ranges?
- How do I serve video from a private bucket?
- How do I serve ranges from my own server?
- Which mistakes should I avoid?
- When should I use HLS or DASH instead of an MP4?
- How to do this on Square Cloud
- FAQs
You upload an 800 MB MP4 to storage, drop the URL into a <video> tag and hit play. It works. Then a viewer drags the seek bar to minute 40 and the video stalls, restarts from zero or downloads everything first. Most of the time the file is fine: the server just isn't answering range requests.
The protocol details below follow RFC 9110 and the MDN documentation.
What is a range request?#
It is an HTTP request that asks for only a piece of a file, measured in bytes. The client sends a Range header, and the server returns just that slice:
GET /videos/lesson.mp4 HTTP/1.1
Host: example.com
Range: bytes=1048576-2097151HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Type: video/mp4
Content-Length: 1048576
Content-Range: bytes 1048576-2097151/838860800The four pieces that matter:
Range: bytes=start-endasks for bytestartthroughend, both inclusive.bytes=0-1023is the first 1024 bytes.bytes=1000-runs to the end.bytes=-500asks for the last 500 bytes.206 Partial Contentmeans the body is only part of the file.Content-Range: bytes 1048576-2097151/838860800says which slice arrived and the total size.Accept-Ranges: bytesadvertises that the server accepts ranges. If the header is missing, the client cannot count on that support.
If the server ignores Range, it answers 200 OK with the whole file. The browser accepts that, but loses the ability to seek without downloading everything.
Why do video and audio depend on it?#
The <video> and <audio> tags do not download a file completely before playing. The browser requests the start, learns the duration and format, and plays while it downloads. When the viewer seeks, it computes the matching byte and fires a new request with Range.
The preload attribute controls how much is downloaded before play: none downloads nothing, metadata downloads only what is needed (duration and dimensions) and auto lets the browser download the whole file. It is a hint, and the browser may ignore it. For pages listing many videos, preload="metadata" avoids downloading dozens of files nobody will play:
<video controls preload="metadata" width="720" src="https://example.com/videos/lesson.mp4"></video>Without range support you get the scenario from the opening: a seek bar that doesn't respond, a slow start and far more bandwidth than needed.
How do I test whether the server accepts ranges?#
Request the first bytes with curl and look only at the headers:
curl -s -o /dev/null -D - -H "Range: bytes=0-99" https://example.com/videos/lesson.mp4A correct response looks like this:
HTTP/2 206
accept-ranges: bytes
content-length: 100
content-range: bytes 0-99/838860800
content-type: video/mp4Check three things: the status is 206 (not 200), content-length is 100 and content-range carries the total. If you get 200 with the full file's content-length, the server ignored Range.
The S3 API works with ranges too: GetObject accepts a Range parameter and returns only that slice. In Python with boto3:
import os
import boto3
s3 = boto3.client(
"s3",
endpoint_url=os.environ.get("S3_ENDPOINT"), # omit on AWS S3
aws_access_key_id=os.environ["S3_ACCESS_KEY"],
aws_secret_access_key=os.environ["S3_SECRET_KEY"],
)
r = s3.get_object(Bucket="my-bucket", Key="videos/lesson.mp4", Range="bytes=0-99")
print(r["ResponseMetadata"]["HTTPStatusCode"]) # 206
print(r["ContentRange"]) # bytes 0-99/838860800
print(len(r["Body"].read())) # 100How do I serve video from a private bucket?#
If the file is not public, generate a presigned URL and use it as the src. By default an S3 presigned URL signs only the host header, so Range is outside the signature and the browser can ask for any slice with the same URL:
url = s3.generate_presigned_url(
"get_object",
Params={"Bucket": "my-bucket", "Key": "videos/lesson.mp4"},
ExpiresIn=6 * 3600,
)Pick ExpiresIn based on how long playback can last, not just on page load: if the URL expires mid-video, the next seek fails with 403. Generate the URL when the viewer opens the player, not at site build time.
How do I serve ranges from my own server?#
If the file sits on your application's disk (or you proxy it), you have to implement Range yourself. This dependency-free Node.js server handles the start-end, start- and -suffix forms:
import { createReadStream, statSync } from "node:fs";
import { createServer } from "node:http";
const FILE = "video.mp4";
createServer((req, res) => {
const { size } = statSync(FILE);
const base = { "Content-Type": "video/mp4", "Accept-Ranges": "bytes" };
const m = /^bytes=(\d*)-(\d*)$/.exec(req.headers.range ?? "");
if (!m || (m[1] === "" && m[2] === "")) {
res.writeHead(200, { ...base, "Content-Length": size });
return createReadStream(FILE).pipe(res);
}
let start, end;
if (m[1] === "") {
start = Math.max(size - Number(m[2]), 0); // bytes=-500: last 500
end = size - 1;
} else {
start = Number(m[1]);
end = 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, {
...base,
"Content-Range": `bytes ${start}-${end}/${size}`,
"Content-Length": end - start + 1,
});
createReadStream(FILE, { start, end }).pipe(res);
}).listen(3000);Test it with the curl command above, pointing at http://localhost:3000. The example ignores multiple ranges (bytes=0-50, 100-150) and returns the whole file, which the specification allows. In production, prefer serving the file from storage or from a web server that already implements this, rather than maintaining this code.
Which mistakes should I avoid?#
moovat the end of the MP4. In an MP4, the video index lives in themoovatom. If it is at the end, the browser has to fetch it before playing. Move it to the start without re-encoding:ffmpeg -i input.mp4 -c copy -movflags +faststart output.mp4. The ffmpeg docs warn that this runs a second pass and does not work with fragmented output.- Getting
200instead of206. Some layer in the path (proxy, compression middleware, serverless function) is droppingRange. Test withcurlstraight against the storage, then against the final URL, to find where it gets lost. - Wrong
Content-Type. An MP4 served asapplication/octet-streammay download instead of playing. Setvideo/mp4,video/webmoraudio/mpegon the object. - Compressing video. Do not apply gzip or brotli to MP4: the file is already compressed, the gain is minimal and a compression layer in the path can disable or break ranges.
- Presigned URL too short. It expires mid-playback and seeking breaks.
- Replacing a file under the same URL. If the object changes while the browser holds cached slices, the pieces do not fit together. Use a new name per version (for example
lesson-v2.mp4) instead of overwriting. - Unwatched bandwidth. Every seek triggers more requests. On storage that charges for outbound traffic, a popular video becomes a cost; read about egress fees before choosing where to host.
When should I use HLS or DASH instead of an MP4?#
An MP4 with ranges covers short and medium videos at a single quality. If you need to switch resolution based on the viewer's connection, or stream live, the path is HLS or DASH: the video is split into small segments and a playlist says which to fetch. The segments are ordinary files, and object storage is a good fit for them. For most sites, starting with an MP4 and faststart is enough.
How to do this on Square Cloud#
Square Cloud's Blob Storage is object storage with an S3-compatible API, and downloads answer range requests with 206, so the player can seek through the video without downloading the whole file. Public files are served by a global edge CDN, and each object supports settings such as Cache-Control and a choice between download and inline display (useful when someone opens the video URL directly in the browser).
For private files, the boto3 code above works with the https://s3-blob.squarecloud.app endpoint, the auto region, the private bucket and the keys from the "S3 access" tab of the dashboard: the SigV4 presigned URL can last up to 7 days. From the dashboard you also get a temporary link of up to 24 hours and a share link of up to 30 days that can be revoked. Files can be up to 10 GB, downloads carry no bandwidth charge, and every plan includes a quota from 5 GB to 250 GB.
Before publishing, run the curl -H "Range: bytes=0-99" test against your object's URL and confirm the 206 and Accept-Ranges. It's the fastest way to check the behavior with your own file.
Blob Storage
S3-compatible object storage with no egress fees
FAQs
Do I need a streaming server to play video on my site?
For a plain MP4 or WebM file, no. The video tag plays the file over HTTP as long as the server or storage answers range requests. Adaptive streaming (HLS or DASH) is only needed if you want to switch quality based on the network.
What does the 416 Range Not Satisfiable error mean?
The server received a Range that does not fit the file, for example a start byte beyond the object size. It answers 416 with Content-Range: bytes */size. It usually means the file changed or the client computed a wrong offset.
Why does the video only start after downloading most of the file?
In an MP4, the metadata lives in the moov atom. If it sits at the end of the file, the browser has to fetch it before playing. Reprocess with ffmpeg -movflags +faststart to move it to the start, without re-encoding.
Does this work with a presigned URL?
Yes. Range is a request header and is not part of an S3 presigned URL's signature. The browser can send Range normally, and the URL only needs to stay valid for the length of playback.
More in Storage
See all 15 articles- What is a presigned (signed) URL?A presigned URL is a temporary link to a private file that works without login or credentials. See how it works, its limits and when to use it.
- What is object storage?Object storage keeps files as objects with a key and metadata, accessed over an HTTP API. See how it works, what it is for and where its limits are.
- What does 'S3-compatible' storage mean?S3 is Amazon's object storage and its API became the industry standard. Learn what S3-compatible means, what differs between providers, and how to connect.
- Uploading large files: multipart and resumable uploadsMultipart upload splits a large file into parts that are sent in parallel and retried on their own. See the Python walkthrough, how to resume, and common mistakes.