Byte Range Request

Protocol

An HTTP GET whose Range header asks for part of a resource, not the whole body — the basis of video seeking, resumable downloads and parallel chunk downloads. A server that honours it replies 206 Partial Content with a Content-Range slice; 416 if nothing is satisfiable, 200 if it ignores Range.

Also known as Range request, Range GET request.

12 min read Updated Aug 30, 2026

Full Explanation

A byte range request is an HTTP GET that carries a Range header. The header asks for only part of a representation: a slice of bytes, not the entire body. Byte range requests are a feature of HTTP itself. They are specified in RFC 9110, which obsoleted the older standalone range specification RFC 7233. A byte range request is not a separate protocol, and it is not a CDN feature, even though CDNs are where it matters most. GET is the only method for which HTTP defines range handling. So a Range header sent with any other method must be ignored (RFC 9110 section 14.2).

A byte range request is not chunked Transfer-Encoding. Chunked Transfer-Encoding frames a response whose total length is not known when sending starts. A range request instead selects bytes from a specific representation. The two are compatible and independent (MDN). Three answers are possible, and a client has to handle all three. The server can send 206 Partial Content carrying the slice. It can send 416 Range Not Satisfiable when none of the requested ranges exists in the representation. Or it can send an ordinary 200 OK with the whole body, because a server is always free to ignore Range.

How it works

The client adds a Range header to a GET. Its value names a range unit and one or more ranges. In practice the unit is always bytes. Other units are extensible in principle, but RFC 9110 records that format-specific boundaries “like pages, sections, records, rows, or time … are not commonly used in practice” (section 16.5.2).

  1. Single inclusive range: bytes=2000-2999 means byte offsets 2000 through 2999. RFC 9110 says “the byte positions specified are inclusive. Byte offsets start at zero” (section 14.1.2).
  2. Open-ended range: bytes=1000- runs from byte 1000 to the end. A last position can be absent, or greater than or equal to the current length. Either way it means “the remainder of the representation” (section 14.1.2).
  3. Suffix range: bytes=-1000 is “the last N units of the representation data” (section 14.1.1). If the representation is shorter than the suffix length, the server returns the entire representation (section 14.1.2).
  4. Multiple ranges: these are comma-separated, for example bytes=0-499,1000-1499. The server answers with a multipart/byteranges body. Each part carries its own Content-Range (section 15.3.7.2). The parts do not have to match the request one for one. A server may coalesce overlapping ranges. It may also coalesce ranges separated by a gap smaller than the roughly 80 bytes of multipart overhead, and it need not send every range asked for. A client that cannot parse a multipart body must not ask for multiple ranges.

On success the server sends 206 with the slice as the body. It also sends a Content-Range header naming the slice and the complete length, for example Content-Range: bytes 1000-1999/52428800. Content-Length describes that slice only. RFC 9110 says it “indicates the number of octets in the content of this message, which is usually not the complete length of the selected representation” (section 15.3.7). Sending the complete length is a SHOULD. An asterisk stands in when the sender does not know it, as in Content-Range: bytes 42-1233/* (section 14.4).

A server advertises support with Accept-Ranges: bytes. It can also send Accept-Ranges: none to advise clients not to try (section 14.3). The advertisement is only advice. A client may send range requests without having seen it. RFC 9110 says a client “MUST NOT assume that receiving an Accept-Ranges field means that future range requests will return partial responses”. Range handling is defined only for GET, so a Range header on a HEAD request is ignored. A HEAD returns 200 and the full Content-Length even where the same GET returns 206. So curl -I is a test of Accept-Ranges, never of slicing.

Byte offsets count the representation’s encoded bytes. RFC 9110 is explicit: “If the representation data has a content coding applied, each byte range is calculated with respect to the encoded sequence of bytes, not the sequence of underlying bytes that would be obtained after decoding” (section 14.1.2). A range over a gzip or brotli response therefore addresses the compressed stream.

For bytes, nothing is satisfiable when the first position is at or past the current length. In that case the server replies 416. It should also add Content-Range: bytes */total so the client learns the current length (section 15.5.17, section 14.4). A 416 is not guaranteed, though. RFC 9110 notes in the same section that servers are free to ignore Range, so many implementations answer with the entire selected representation in a 200 instead. Clients therefore cannot depend on receiving a 416, even when it is the most appropriate response.

To resume a transfer safely, the client sends If-Range alongside Range. This means: “if the representation is unchanged, send me the part(s) that I am requesting in Range; otherwise, send me the entire representation” (section 13.1.5). That way bytes from two versions are never stitched into one file. The validator has to be strong. A client must not put a weak ETag in If-Range. A date only counts if it is a strong validator.

Why it matters for a CDN

Range requests are the mechanism behind seeking in a video, resuming an interrupted download, and reading only the part of a large file a tool needs. In MDN’s words, they serve “media players that support random access, data tools that require only part of a large file, and download managers that let users pause and resume a download” (MDN). Most of that traffic runs on CDNs, and most of it is video. AppLogic Networks, formerly Sandvine, reports for 2025 that “video remains the largest application category in terms of volume” (AppLogic Networks). Its dated figure from the 2023 report is more concrete: “video usage grew 24% in 2022, now equating to 65% of all internet traffic” (Sandvine, January 2023).

Ranges are also structural in streaming, not just a seek trick. An HLS playlist can address a media segment as a sub-range of a bigger file with the EXT-X-BYTERANGE tag (RFC 8216 section 4.3.2.2). The spec ties that straight to this mechanism: “If a server supports partial loading of resources (e.g., via HTTP Range requests), it MAY specify segments as sub-ranges of larger resources using the EXT-X-BYTERANGE tag” (section 6.2.1).

So what the edge does with a range decides two things: the latency a viewer feels on a seek, and the load on the origin. An edge that holds whole objects and slices locally answers seeks from cache. An edge that forwards every range turns one file into a stream of origin fetches. Ranges also let a CDN deliver objects it will not cache whole. CloudFront refuses to cache anything over 50 GB. Yet a viewer can pull a 100 GB object in sub-50 GB parts that CloudFront caches individually (CloudFront docs).

What CDNs do

Range support is on by default at all three providers below, but what they cache differs.

  • Cloudflare serves ranges from its cache, with one condition: “If the origin response includes a Content-Length header, then the specified byte range will be returned with an HTTP 206 response. If the origin response does not include the Content-Length header, the cache will return the full content with an HTTP 200 response.” No opt-in is needed, but the object must be cacheable first. Cloudflare “only caches based on file extension and not by MIME type”, and MP4 is on the default list. Where the origin sends no Cache-Control or Expires, a 206 gets the same default 120-minute edge TTL as a 200.
  • Fastly caches the whole object instead. The readthrough cache “automatically transforms a ranged request into a request for the entire body when forwarding it to the backend, so that the cache always contains the entire object”. It then turns that cached object back into a 206 for the client. Later requests for the whole object, or for “a range (even a different range)”, are answered from the same copy. Simultaneous requests for different ranges are collapsed into one backend fetch. Fastly calls this range collapsing. In a CDN service the readthrough interface “works without any configuration or code required”.
  • AWS CloudFront serves from the edge cache when it already holds the whole object or the requested part. Otherwise it forwards to the origin, where “To optimize performance, CloudFront may request a larger range than the client requested in the Range GET”. If the origin does not support ranges, it returns the entire object. CloudFront serves and caches that entire object, then answers later range requests from the same copy. Range lists must be ascending, non-overlapping and valid. Otherwise CloudFront “returns HTTP status code 200 with the full object instead of status code 206”.
  • Self-run caches can slice on a fixed grid instead. nginx’s slice module “splits a request into subrequests, each returning a certain range of response”, and so “provides more effective caching of big responses”. It is not built by default. The $slice_range variable must be added to the cache key, with caching of 206 responses enabled (nginx docs). That is the shape of the configuration in this page’s examples.

Watch out for

  • Offsets are computed on compressed bytes. This surprises people who slice a file at the origin and then enable compression. CloudFront states it plainly: “For a range request for a compressed object, the byte range request is based on the compressed size, and not the original size of the object” (CloudFront docs).
  • 200 is a legal answer to a range request. A server “MAY ignore the Range header field”. An origin server must ignore a Range header field carrying a range unit it does not understand, and “A proxy MAY discard a Range header field that contains a range unit it does not understand” (RFC 9110 section 14.2). CloudFront also falls back to the full object when the range list breaks its syntax rules, and when the origin answers with Transfer-Encoding: chunked, CloudFront “returns the entire object to the viewer instead of the requested range” (CloudFront docs).
  • Cacheable-size caps are per provider and per plan. They bite hardest on the large media that ranges exist for. Cloudflare caps cacheable files at 512 MB on Free, Pro and Business, and at 5 GB by default on Enterprise (Cloudflare docs). CloudFront closes the origin connection and errors when Content-Length shows an object above 50 GB with caching enabled (CloudFront docs).
  • Never stitch slices without one strong validator. Partial responses “can only be safely combined if they all have in common the same strong validator” (RFC 9110 section 15.3.7.3). A cache may combine stored ranges only “if they all share the same strong validator” (RFC 9111 section 3.4).
  • Partial responses are cacheable but must stay marked as partial. A 206 “MAY be stored as if it were an incomplete 200 (OK) response” and completed later, but “A cache MUST NOT send a partial response to a client without explicitly marking it using the 206 (Partial Content) status code” (RFC 9111 section 3.3).
  • Many small or overlapping ranges look like an attack. A server that supports ranges may ignore or reject a specifier with “more than two overlapping ranges, or a set of many small ranges that are not listed in ascending order” (RFC 9110 section 14.2), and 416 is the documented answer to “an excessive number of small or overlapping ranges (a potential denial of service attack)” (section 15.5.17).

Best practice

  • Probe with a HEAD request for Accept-Ranges: bytes. Treat the answer as a hint. MDN reads its absence as no support for partial requests, while RFC 9110 makes its presence advisory (MDN, RFC 9110 section 14.3).
  • Test slicing with a GET, not with curl -I. Run curl -sD - -o /dev/null -H "Range: bytes=0-999" <url>. It should show 206, a Content-Range ending in the total size, and Content-Length: 1000. A 200 means the layer you tested did not slice.
  • Cache whole objects and slice at the edge, as Fastly and CloudFront do, or cache uniform fixed-size slices, as nginx’s slice module does. What wastes edge storage is a separate entry per arbitrary client range, not slicing itself.
  • Resume with If-Range plus a strong ETag, so a changed object yields a fresh 200 rather than a slice of the old one (RFC 9110 section 13.1.5).
  • Ask for few, large, ascending ranges. A client “SHOULD NOT request multiple ranges that are inherently less efficient to process and transfer than a single range that encompasses the same data” (RFC 9110 section 14.2).
  • Reach for ranges deliberately when an object exceeds what the CDN will cache. CloudFront’s own worked example pulls a 100 GB object in 20 GB parts, each cached separately (CloudFront docs).

Examples

Request one byte range:

# Request bytes 0-999 (first 1KB)
curl -H "Range: bytes=0-999" -sI https://cdn.example.com/video.mp4
# HTTP/2 206
# Content-Range: bytes 0-999/52428800
# Content-Length: 1000

# Request last 1KB
curl -H "Range: bytes=-1000" -sI https://cdn.example.com/video.mp4

# Resume a failed download from byte 10485760
curl -C 10485760 -o video.mp4 https://cdn.example.com/video.mp4

Nginx serves ranges from its cached copies:

# Enable slicing for large files (caches full object, serves ranges)
proxy_cache_path /cache levels=1:2 keys_zone=video:10m max_size=50g;

server {
    location /video/ {
        slice 1m;  # fetch from origin in 1MB slices
        proxy_cache video;
        proxy_cache_key $uri$slice_range;
        proxy_set_header Range $slice_range;
        proxy_cache_valid 200 206 24h;
    }
}

Frequently Asked Questions

An HTTP GET whose Range header asks for part of a resource, not the whole body — the basis of video seeking, resumable downloads and parallel chunk downloads. A server that honours it replies 206 Partial Content with a Content-Range slice; 416 if nothing is satisfiable, 200 if it ignores Range.

Request one byte range:

# Request bytes 0-999 (first 1KB)
curl -H "Range: bytes=0-999" -sI https://cdn.example.com/video.mp4
# HTTP/2 206
# Content-Range: bytes 0-999/52428800
# Content-Length: 1000

# Request last 1KB
curl -H "Range: bytes=-1000" -sI https://cdn.example.com/video.mp4

# Resume a failed download from byte 10485760
curl -C 10485760 -o video.mp4 https://cdn.example.com/video.mp4

Nginx serves ranges from its cached copies:

# Enable slicing for large files (caches full object, serves ranges)
proxy_cache_path /cache levels=1:2 keys_zone=video:10m max_size=50g;

server {
    location /video/ {
        slice 1m;  # fetch from origin in 1MB slices
        proxy_cache video;
        proxy_cache_key $uri$slice_range;
        proxy_set_header Range $slice_range;
        proxy_cache_valid 200 206 24h;
    }
}

Yes. Byte Range Request is also known as Range request, Range GET request. An HTTP GET whose Range header asks for part of a resource, not the whole body — the basis of video seeking, resumable downloads and parallel chunk downloads. A server that honours it replies 206 Partial Content with a Content-Range slice; 416 if nothing is satisfiable, 200 if it ignores Range.

Related CDN concepts include:

  • Content-Encoding — The HTTP header naming the content codings applied to a body (gzip, br, zstd), which …