Skip to content

206 Partial Content

What is HTTP 206 Partial Content?

206 Partial Content
When you watch a video online and drag the slider to the middle, your computer a...
HTTP 206 Partial Content status code illustration

Explain Like I’m 3

You asked for a piece of something big, not the whole thing! Like asking for just 3 cookies from a big jar instead of all of them. You get exactly what you asked for - just that piece!

Example: You have a really long coloring book, but you only want pages 5, 6, and 7. Someone gives you just those 3 pages, not the whole book!

Explain Like I’m 5

When you watch a video online and drag the slider to the middle, your computer asks the website ‘Can I have just the part starting from minute 5?’ The website says ‘206 Partial Content’ and sends just that piece of the video, not the whole thing from the beginning! This is really useful because downloading the whole video would take forever and waste space. It also lets you skip around in videos, download really big files a little bit at a time, or continue a download if it got interrupted. The website keeps track of which ‘piece’ it’s sending you.

Example: You’re watching a 1-hour movie online. You click to skip to minute 30. Your computer asks for ‘bytes 500,000 to 1,000,000’ (the part starting at minute 30). The server responds with ‘206 Partial Content’ and sends just that chunk, so the video starts playing from minute 30 immediately!

Jr. Developer

HTTP 206 Partial Content is returned when a client requests part of a resource using Range headers, and the server supports range requests. The client sends Range: bytes=0-1023 to request the first 1KB, and the server responds with 206 and Content-Range: bytes 0-1023/50000 indicating ‘here’s bytes 0-1023 out of 50000 total.’ Use cases include: video/audio streaming (seek to any position without downloading the whole file), resumable downloads (if download fails at byte 1000000, resume from there instead of restarting), large file downloads in chunks (download 10MB at a time), PDF viewers loading pages on-demand. The server indicates range support with Accept-Ranges: bytes in responses. If ranges aren’t supported, the server returns 200 with the full resource. You can request multiple ranges in one request (multipart/byteranges response). Conditional range requests combine If-Range header with Range to ensure the resource hasn’t changed since the last chunk.

Example: A user is downloading a 500MB video file. At byte 250,000,000, the download fails. Instead of restarting from byte 0, the client sends Range: bytes=250000000- (requesting ‘from byte 250M to end’). The server responds with 206 and Content-Range: bytes 250000000-524287999/524288000, resuming the download from where it left off.

Code Example

// Express.js handling range requests
const fs = require('fs');
const path = require('path');
app.get('/videos/:filename', (req, res) => {
const filePath = path.join(__dirname, 'videos', req.params.filename);
const stat = fs.statSync(filePath);
const fileSize = stat.size;
const range = req.headers.range;
if (range) {
// Parse Range header: "bytes=1024-2047"
const parts = range.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
const chunkSize = (end - start) + 1;
const fileStream = fs.createReadStream(filePath, { start, end });
res.status(206)
.set({
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'Accept-Ranges': 'bytes',
'Content-Length': chunkSize,
'Content-Type': 'video/mp4'
});
fileStream.pipe(res);
} else {
// No range requested, send whole file
res.status(200)
.set({
'Content-Length': fileSize,
'Content-Type': 'video/mp4',
'Accept-Ranges': 'bytes'
});
fs.createReadStream(filePath).pipe(res);
}
});

Crash Course

206 Partial Content, defined in RFC 9110 Section 15.3.7, indicates the server is delivering only part of the resource due to a Range header in the request. Range requests enable clients to request specific byte ranges of a resource, essential for video streaming, large file downloads, and bandwidth optimization. The Range header syntax is Range: bytes=<start>-<end>, where both boundaries are optional: bytes=0-1023 (first 1KB), bytes=1024- (from byte 1024 to end), bytes=-500 (last 500 bytes), bytes=0-0,-1 (first and last byte). The server responds with Content-Range: bytes <start>-<end>/<total> indicating which portion is being sent. The server MUST include Content-Range header in 206 responses. Servers indicate range support by including Accept-Ranges: bytes in responses (or Accept-Ranges: none if not supported). If the Range header is satisfiable, return 206; if unsatisfiable (e.g., bytes beyond file size), return 416 Range Not Satisfiable; if ranges aren’t supported, ignore Range and return 200 with full resource. Conditional range requests use If-Range header with an ETag or date - if the resource hasn’t changed, return 206 with the range; if changed, ignore Range and return 200 with the full new version (prevents assembling mismatched chunks). Multiple ranges in one request (Range: bytes=0-100,200-300) result in multipart/byteranges response with MIME boundaries separating each chunk. Caching: 206 responses are cacheable, with ETag/Last-Modified for validation. Content-Length in 206 indicates the range size, not the full resource size (Content-Range provides total). Common in video players (seeking), download managers (resume), CDNs (efficient delivery), and mobile apps (bandwidth conservation).

Example: A video streaming app plays a 2GB movie (2,147,483,648 bytes). User clicks to skip to 50% through. The client calculates byte offset: 1,073,741,824 and sends Range: bytes=1073741824-. Server responds 206 with Content-Range: bytes 1073741824-2147483647/2147483648 and starts streaming from that point. As playback continues, the client requests subsequent chunks (bytes 1073741824-1074790399, 1074790400-1075838975, etc.). If the user seeks again, a new range request is sent. This avoids downloading the first 1GB unnecessarily, saving bandwidth and enabling instant seeking.

Code Example

// Production video streaming with range support
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
app.get('/api/videos/:id/stream', async (req, res) => {
const videoId = req.params.id;
// Get video metadata from database
const video = await db.getVideo(videoId);
if (!video) {
return res.status(404).json({ error: 'Video not found' });
}
const filePath = path.join(__dirname, 'videos', video.filename);
// Check if file exists
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'Video file not found' });
}
const stat = fs.statSync(filePath);
const fileSize = stat.size;
const range = req.headers.range;
// Always indicate we support range requests
res.set('Accept-Ranges', 'bytes');
if (range) {
// Parse range header
const parts = range.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
// Validate range
if (start >= fileSize || end >= fileSize) {
return res.status(416)
.set('Content-Range', `bytes */${fileSize}`)
.json({ error: 'Range not satisfiable' });
}
const chunkSize = (end - start) + 1;
const fileStream = fs.createReadStream(filePath, { start, end });
// Return 206 Partial Content
res.status(206)
.set({
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'Content-Length': chunkSize,
'Content-Type': 'video/mp4',
'Cache-Control': 'public, max-age=31536000', // Cache chunks
'ETag': video.etag
});
fileStream.pipe(res);
} else {
// No range requested - send entire file
res.status(200)
.set({
'Content-Length': fileSize,
'Content-Type': 'video/mp4',
'Cache-Control': 'public, max-age=31536000',
'ETag': video.etag
});
fs.createReadStream(filePath).pipe(res);
}
});
// Client-side: Video player using range requests
async function playVideo(videoId, seekToPosition) {
const video = document.querySelector('video');
const videoUrl = `/api/videos/${videoId}/stream`;
// Calculate byte offset (assuming 1MB/second bitrate)
const bytesPerSecond = 1048576;
const startByte = Math.floor(seekToPosition * bytesPerSecond);
const response = await fetch(videoUrl, {
headers: {
'Range': `bytes=${startByte}-`
}
});
if...

Deep Dive

The 206 Partial Content status code, specified in RFC 9110 Section 15.3.7, indicates that the server is successfully fulfilling a range request for the target resource by transferring one or more parts of the selected representation. Range requests, defined in RFC 9110 Section 14, allow clients to request specific byte ranges of a resource, enabling efficient delivery of large files, random access to media, and resumable downloads. The 206 response contains one or more ranges of the selected representation, depending on whether the Range header specified a single range or multiple ranges.

Technical Details

Range request syntax follows RFC 9110: Range: bytes=<range-spec>, where <range-spec> can be <start>-<end> (inclusive byte positions), <start>- (from start to end of file), -<suffix-length> (last N bytes), or multiple ranges separated by commas. Examples: Range: bytes=0-499 (first 500 bytes), Range: bytes=500- (from byte 500 to end), Range: bytes=-500 (last 500 bytes), Range: bytes=0-99,200-299 (bytes 0-99 and 200-299). Byte positions are zero-indexed.\n\nThe Content-Range response header is REQUIRED in 206 responses. Syntax: Content-Range: bytes <start>-<end>/<total> or Content-Range: bytes /<total> for 416 responses when range is unsatisfiable. Example: Content-Range: bytes 1000-1999/50000 means ‘bytes 1000-1999 out of 50000 total.’ If the total size is unknown, use * instead: Content-Range: bytes 1000-1999/.\n\nConditional range requests combine Range with If-Range header to prevent assembling chunks from different versions of a resource. The If-Range value can be an ETag or Last-Modified date. Behavior: if If-Range matches current resource, return 206 with range; if doesn’t match, ignore Range and return 200 with full resource. This ensures cache coherency when resuming downloads of potentially modified files. Example: If-Range: “abc123” with Range: bytes=1000000- returns 206 if ETag still “abc123”, otherwise returns 200 with full new file.\n\nMultipart range responses occur when multiple non-contiguous ranges are requested. The Content-Type becomes multipart/byteranges with a boundary, and each range is a separate body part with its own Content-Range and Content-Type headers. Example response format:\n\nHTTP/1.1 206 Partial Content\nContent-Type: multipart/byteranges; boundary=BOUNDARY\n\n--BOUNDARY\nContent-Type: video/mp4\nContent-Range: bytes 0-99/50000\n[bytes 0-99]\n--BOUNDARY\nContent-Type: video/mp4\nContent-Range: bytes 200-299/50000\n[bytes 200-299]\n--BOUNDARY--\n\nHowever, multipart ranges are rarely used in practice - most implementations request ranges sequentially.\n\nThe 416 Range Not Satisfiable response is returned when the requested range is outside the resource bounds. For instance, Range: bytes=60000- on a 50000-byte file returns 416 with Content-Range: bytes */50000. Clients should retry with adjusted ranges or request the full resource.\n\nCaching implications: 206 responses are cacheable by default. Caches must store the range specified in Content-Range, not the full resource. ETag and Last-Modified enable cache validation. A cache hit for a range can satisfy future requests for the same range. However, caches typically don’t assemble multiple cached ranges into a full resource - each range is cached independently. The Vary header should include Range if response varies by range (though this is implicit).\n\nContent-Length in 206 responses indicates the size of the range being sent, NOT the full resource size. The full size is in Content-Range. Example: a 50000-byte file with Range: bytes=1000-1999 returns C…

Code Example

// Enterprise-grade range request handling\nconst express = require('express');\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst app = express();\n\n// Configuration\nconst MAX_RANGES_PER_REQUEST = 5;\nconst MIN_RANGE_SIZE = 1024; // 1KB minimum\nconst DEFAULT_CHUNK_SIZE = 1048576; // 1MB\n\n// Parse Range header\nfunction parseRange(rangeHeader, fileSize) {\n if (!rangeHeader || !rangeHeader.startsWith('bytes=')) {\n return null;\n }\n \n const rangeStr = rangeHeader.replace('bytes=', '');\n const ranges = rangeStr.split(',').map(r => r.trim());\n \n if (ranges.length > MAX_RANGES_PER_REQUEST) {\n throw new Error('Too many ranges requested');\n }\n \n return ranges.map(range => {\n const parts = range.split('-');\n let start, end;\n \n if (parts[0] === '') {\n // Suffix range: -500 (last 500 bytes)\n start = Math.max(0, fileSize - parseInt(parts[1]));\n end = fileSize - 1;\n } else if (parts[1] === '') {\n // Open-ended: 1000- (from 1000 to end)\n start = parseInt(parts[0]);\n end = fileSize - 1;\n } else {\n // Closed range: 1000-1999\n start = parseInt(parts[0]);\n end = parseInt(parts[1]);\n }\n \n // Validate\n if (isNaN(start) || isNaN(end) || start < 0 || end >= fileSize || start > end) {\n throw new Error('Invalid range');\n }\n \n // Enforce minimum range size (anti-abuse)\n if ((end - start + 1) < MIN_RANGE_SIZE && (end - start + 1) < fileSize) {\n end = Math.min(start + MIN_RANGE_SIZE - 1, fileSize - 1);\n }\n \n return { start, end };\n });\n}\n\n// Generate ETag for file\nfunction generateETag(filePath, stat) {\n return '\"' + crypto.createHash('md5')\n .update(filePath + stat.mtime.toISOString() + stat.size)\n .digest('hex') + '\"';\n}\n\napp.get('/api/files/:id/download', async (req, res) => {\n const fileId = req.params.id;\n \n // Get file metadata\n const fileRecord = await db.getFile(fileId);\n if (!fileRecord) {\n return res.status(404).json({ error: 'File not found' });\n }\n \n // Authorization check\n if (!await authService.canAccessFile(req.user.id, fileId)) {\n return res.status(403).json({ error: 'Forbidden' });\n }\n \n const filePath = path.join(__dirname, 'files', fileRecord.storagePath);\n \n // Check file exists\n if (!fs.existsSync(filePath)) {\n console.error('File missing from storage:', filePath);\n return res.status(500).json({ error: 'File storage error' });\n }\n \n const stat = fs.statSync(filePath);\n const fileSize = stat.size;\n const etag = generateETag(filePath, stat);\n const lastModified = stat.mtime.toUTCString();\n \n // Always indicate range support\n res.set('Accept-Ranges', 'bytes');\n \n // Handle If-Range conditional request\n const ifRange = req.headers['if-range'];\n if (ifRange) {\n // Check if resource matches If-Range condition\n const matches = ifRange === etag || i...

Frequently Asked Questions

How do I request a specific range of a file?

Send a Range header with your request: Range: bytes=1000-1999 for bytes 1000-1999, Range: bytes=1000- for byte 1000 to end, or Range: bytes=-500 for the last 500 bytes. The server returns 206 with Content-Range indicating which bytes are in the response. If the range is invalid or unsupported, you'll get 416 Range Not Satisfiable or 200 with the full file.

How do resumable downloads work with 206?

When a download fails at byte 1,000,000, the client stores the ETag or Last-Modified from the initial response. To resume, send If-Range with the stored validator and Range: bytes=1000000- for remaining bytes. If the file hasn't changed, you get 206 with the rest. If it changed, you get 200 with the full new file, preventing corruption from mismatched chunks.

What's the difference between Content-Length and Content-Range?

In 206 responses, Content-Length indicates the size of the chunk being sent (e.g., 1000 bytes for a 1KB chunk), while Content-Range shows the chunk's position in the full file (e.g., bytes 1000-1999/50000 means this chunk is bytes 1000-1999 out of a 50KB total file). Content-Range provides the full file size after the slash.

Can I request multiple ranges in one request?

Yes, using Range: bytes=0-99,200-299 requests bytes 0-99 and 200-299. The server returns 206 with Content-Type: multipart/byteranges and each range as a separate MIME part with its own Content-Range header. However, this is rarely used in practice - most clients request ranges sequentially instead.

How do video players use 206 for seeking?

When you seek in a video, the browser calculates the byte offset for that timestamp and sends Range: bytes=<offset>-. The server returns 206 with the video data starting from that point. The browser can immediately play from the new position without downloading the entire file. Each seek generates a new range request.

Common Causes

  • Video or audio streaming with seeking/scrubbing functionality
  • Resumable file downloads in download managers
  • Large file downloads requested in chunks (10MB at a time)
  • PDF viewers loading specific pages on-demand
  • Mobile apps conserving bandwidth by requesting partial resources
  • CDN edge servers delivering byte-range chunks efficiently
  • HTTP Live Streaming (HLS) or DASH adaptive bitrate streaming
  • Image galleries loading high-resolution images progressively

Implementation Guidance

  • Server-side: Parse Range header and extract start/end byte positions
  • Server-side: Validate range is within file bounds (0 to fileSize-1)
  • Server-side: Return 416 Range Not Satisfiable if range is invalid
  • Server-side: Use fs.createReadStream({start, end}) for efficient chunk reading
  • Server-side: Include Accept-Ranges: bytes in all file responses
  • Server-side: Set Content-Range: bytes <start>-<end>/<total> in 206 responses
  • Server-side: Set Content-Length to chunk size, not full file size
  • Server-side: Support If-Range for safe resumable downloads
  • Server-side: Limit maximum ranges per request to prevent abuse
  • Client-side: Send Range header with desired byte positions
  • Client-side: Check for 206 status - if 200, you got the full file
  • Client-side: Parse Content-Range to know what bytes you received
  • Client-side: Store ETag/Last-Modified for resumable downloads
  • CDN: Enable byte-range caching for better performance
📚 Sources:
🕐 Last updated: January 5, 2026

Comments