Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lab 11 · Range Requests & Byte Serving

Run it: make lab-11
Source: labs/lab-11-range-requests/main.go


The Problem

A user seeks to 00:45:00 in a 4-hour movie. The full file is 4 GB. Without range requests, the client must:

  1. Start streaming from the beginning
  2. Buffer through 2.8 GB before reaching the 45-minute mark
  3. Or re-download the entire file after a connection drop

This is obviously unacceptable. HTTP Range Requests (RFC 7233) solve this by allowing clients to request specific byte ranges of a resource.


HTTP Range Requests: RFC 7233

The Request

GET /video/movie.mp4 HTTP/1.1
Range: bytes=2097152-4194303

Requesting bytes 2,097,152 to 4,194,303 (a 2 MB chunk starting at the 2 MB mark).

The Response

HTTP/1.1 206 Partial Content
Content-Range: bytes 2097152-4194303/10485760
Content-Length: 2097152
Content-Type: video/mp4

206 Partial Content indicates a successful range request. The full resource size is 10,485,760 (10 MB) in this example.

Byte Range Syntax

FormatMeaning
bytes=0-499First 500 bytes
bytes=500-999Second 500 bytes
bytes=-500Last 500 bytes
bytes=9500-Bytes from 9500 to end
bytes=0-0,-1First and last byte

Multipart Range Responses

A single request can ask for multiple disjoint ranges:

Range: bytes=0-50, 100-150

Response:

HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=3d6b6a416f9b5

--3d6b6a416f9b5
Content-Type: text/plain
Content-Range: bytes 0-50/1270

[50 bytes of data]
--3d6b6a416f9b5
Content-Type: text/plain
Content-Range: bytes 100-150/1270

[50 bytes of data]
--3d6b6a416f9b5--

Multipart ranges are rarely used in practice — most video players request sequential single ranges.

Accept-Ranges Header

The server advertises range request support:

Accept-Ranges: bytes

If absent or Accept-Ranges: none, the client knows not to bother with range requests.


How the CDN Handles Range Requests

Case 1: Full object cached

If the CDN has the full object cached, it can serve any range locally without contacting the origin:

func serveRange(w http.ResponseWriter, r *http.Request, body []byte) {
    // Parse Range header
    start, end := parseRange(r.Header.Get("Range"), len(body))
    
    w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, len(body)))
    w.Header().Set("Content-Length", strconv.Itoa(end-start+1))
    w.WriteHeader(http.StatusPartialContent)
    w.Write(body[start : end+1])
}

Case 2: Object not cached (miss)

The CDN must fetch the object from origin. Two strategies:

  1. Fetch full object: Request the complete file, cache it, serve the requested range. Simpler, but wasteful if the user only watches 5 minutes of a 4-hour movie.

  2. Forward range request: Pass the Range header upstream. The origin returns the exact bytes requested, which the CDN serves and caches. Problem: the CDN now has a partial object cached. Subsequent requests for different ranges must all go to origin.

Most CDNs use a hybrid approach: Range request forwarding with background fetch of the full object. Serve the requested range immediately (low TTFB), fetch the rest in background for future requests.

Case 3: Partial object cached (common in video)

A popular approach is segment-based caching: the CDN maps the byte range to a fixed-size segment (e.g., 1 MB) and caches segments independently. Any range request is decomposed into cached segments plus (at most) two uncached boundary segments.

This is what Akamai Adaptive Media Delivery and AWS CloudFront Media Store do internally for large video files.


Video Player Behavior

HTML5 video players (<video>) issue range requests with a characteristic pattern:

  1. Initial fetch: Range: bytes=0-65535 (first 64 KB — moov atom for MP4)
  2. Seek: Range: bytes=<offset of target timestamp>-<offset+1MB>
  3. Buffer ahead: sequential range requests slightly ahead of playback
  4. On pause: cancel in-flight range request
  5. On resume: restart from current position

The CDN sees these as a sequence of range requests to the same URL. Caching the full object ensures all of these can be served locally after the first full miss.


Download Resumption

When a large file download is interrupted (connection drop, browser closed):

Resumed download:
GET /downloads/large-file.zip
Range: bytes=52428800-

↑ Resume from exactly where it stopped (50 MB mark)

Without range support, the user restarts the entire download. With range support, the download resumes from where it was.

The CDN must set ETag or Last-Modified on the initial response so the client can validate the resource hasn’t changed before resuming:

If-Range: "abc123"
Range: bytes=52428800-

If-Range says: “If the ETag still matches, resume; otherwise send the full file again.” This prevents serving corrupt data if the file was updated between the initial download and the resume.


Implementation: http.ServeContent

Go’s standard library provides a complete range request implementation:

func handler(w http.ResponseWriter, r *http.Request) {
    body := getContent(r.URL.Path)
    reader := bytes.NewReader(body)
    modTime := time.Now() // or real modification time
    http.ServeContent(w, r, r.URL.Path, modTime, reader)
}

http.ServeContent handles:

  • Range header parsing and validation
  • 206 Partial Content responses
  • 304 Not Modified via If-Modified-Since and If-None-Match
  • Content-Range header generation
  • Multipart ranges

For CDN caching layers, the lab implements manual range handling to show the full mechanics. For production use of static files, http.ServeContent or http.ServeFile are correct choices.


What to Measure

# Ratio of 206 vs 200 responses (high ratio = lots of video/download traffic)
rate(http_responses_total{status="206"}[5m]) /
  rate(http_responses_total{status="200"}[5m])

# Range request cache hit ratio
rate(cache_hits_total{request_type="range"}[5m]) /
  rate(cache_requests_total{request_type="range"}[5m])

# Large object hit ratio (bytes, not requests — often more meaningful)
rate(cache_hit_bytes_total[5m]) /
  rate(cache_total_bytes_total[5m])

Try It

make lab-11

# Full file
curl http://localhost:8080/file/video.mp4 -v

# First 10 KB
curl http://localhost:8080/file/video.mp4 -H "Range: bytes=0-10239" -v

# Last 4 KB
curl http://localhost:8080/file/video.mp4 -H "Range: bytes=-4096" -v

# Resume from 50 MB
curl http://localhost:8080/file/large.bin -H "Range: bytes=52428800-" -v

# With If-Range (ETag-validated resume)
ETAG=$(curl -si http://localhost:8080/file/video.mp4 | grep -i etag | awk '{print $2}')
curl http://localhost:8080/file/video.mp4 \
  -H "Range: bytes=1024-2047" \
  -H "If-Range: $ETAG" -v