{"id":"design-video-streaming","title":"Design a video streaming platform","sections":[{"id":"overview","title":"Overview","html":"<p>A video platform lets creators upload videos and lets viewers watch them on anything from a phone on a train to a living-room TV. The upload side is a batch pipeline that turns one large source file into dozens of smaller ones. The watch side is almost entirely a content delivery problem: moving a very large number of bytes to viewers cheaply, without stalls.</p>\n<p>Interviewers like this question because the interesting parts aren't in the web tier. They're in how you get multi-gigabyte files in reliably, how you split transcoding into parallel work, how adaptive bitrate streaming actually works, and why bandwidth cost shapes the architecture. It builds on the primer's <a href=\"/learn/content-delivery-network\">CDN</a> and <a href=\"/learn/asynchronism\">asynchronism</a> chapters.</p>","headings":[]},{"id":"requirements-and-estimates","title":"Requirements and estimates","html":"<h3 id=\"requirements-and-estimates\">Requirements and estimates</h3>\n<p>Scope:</p>\n<ul>\n<li><strong>Functional:</strong> creators upload videos up to a few hours long; the platform transcodes them and publishes them; viewers browse, watch with smooth quality switching, and see view counts.</li>\n<li><strong>Non-functional:</strong> playback starts within a couple of seconds and rarely stalls; uploads survive flaky connections; a published video is never lost; processing a typical video takes minutes, not hours.</li>\n<li><strong>Out of scope:</strong> live streaming, recommendations, comments, monetization. Mention them, then set them aside.</li>\n</ul>\n<p>The numbers below are assumptions for a mid-to-large platform, chosen to keep the arithmetic readable.</p>\n<h4 id=\"watching\">Watching</h4>\n<ul>\n<li>40 million daily viewers, each watching 45 minutes a day.</li>\n<li>Average delivered bitrate of 3 Mbps across phones, laptops and TVs.</li>\n<li>Average concurrent viewers: 40M × 45 min ÷ 1,440 min a day = <strong>1.25 million</strong>.</li>\n<li>Average egress: 1.25M × 3 Mbps = 3.75 Tbps. With an evening peak of about 2.5× average, that's roughly <strong>9 Tbps</strong>.</li>\n<li>Bytes a day: 3.75 × 10¹² bits/s × 86,400 s = 3.24 × 10¹⁷ bits ≈ 4.05 × 10¹⁶ bytes, about <strong>40 PB a day</strong>.</li>\n</ul>\n<h4 id=\"uploading\">Uploading</h4>\n<ul>\n<li>500,000 new videos a day, averaging 8 minutes.</li>\n<li>Source files average 10 Mbps: 8 × 60 s × 10 Mbps = 4,800 Mb = 600 MB each.</li>\n<li>Raw uploads: 500K × 600 MB = <strong>300 TB a day</strong>, or 300 TB ÷ 86,400 s ≈ 3.5 GB/s ≈ 28 Gbps of ingress.</li>\n<li>Transcoded output: if the rendition ladder below adds up to about 10 Mbps, roughly the source bitrate, the outputs are about the same size as the inputs: another 300 TB a day per codec.</li>\n<li>Total new storage: about 600 TB a day, or roughly 220 PB a year before replication.</li>\n</ul>\n<p>The headline is the ratio. Egress (about 40 PB a day) is over a hundred times ingress (300 TB a day). Read bandwidth is the dominant cost, so the design should spend money on the write side, like extra encoding work, if it saves bytes on the read side. <a href=\"/learn/estimation\">Estimation</a> covers this kind of reasoning.</p>","headings":[{"id":"requirements-and-estimates","title":"Requirements and estimates","depth":3},{"id":"watching","title":"Watching","depth":4},{"id":"uploading","title":"Uploading","depth":4}]},{"id":"api-and-data-model","title":"API and data model","html":"<h3 id=\"api-and-data-model\">API and data model</h3>\n<p>Uploads use a short REST handshake; the bytes themselves bypass your servers:</p>\n<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">POST</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> /videos                         -> {video_id, upload_id, part_urls[]}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">POST</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> /videos/{id}/upload:complete    {upload_id, parts: [{n, etag}]}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">GET</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  /videos/{id}                    -> metadata, status, thumbnails</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">GET</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\">  /videos/{id}/play               -> {manifest_url, license_url?}</span></span>\n<span class=\"line\"><span style=\"color:#D73A49;--shiki-dark:#F97583\">POST</span><span style=\"color:#24292E;--shiki-dark:#E1E4E8\"> /videos/{id}/views              {session_id, position}</span></span></code></pre>\n<p><code>GET /play</code> returns the URL of a manifest on the CDN, not the video. The player fetches everything else from the CDN.</p>\n<p>The main records:</p>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n<div class=\"table-wrap\"><table><thead><tr><th>Record</th><th>Store</th><th>Notes</th></tr></thead><tbody><tr><td><code>videos</code></td><td>Sharded relational DB</td><td>id, owner, title, duration, status (<code>uploading</code>, <code>processing</code>, <code>ready</code>, <code>blocked</code>), visibility</td></tr><tr><td><code>renditions</code></td><td>Same DB</td><td>video_id, codec, resolution, bitrate, manifest path</td></tr><tr><td><code>jobs</code> / <code>tasks</code></td><td>Workflow store</td><td>DAG state per video, retries, timings</td></tr><tr><td><code>view_counts</code></td><td>Counter store</td><td>Aggregated, updated in batches</td></tr><tr><td>Source and segments</td><td>Object storage</td><td>Immutable files addressed by path</td></tr></tbody></table></div>\n<p>Metadata is small, relational and read-heavy, so a sharded SQL database with a cache in front works well. YouTube built Vitess to scale its MySQL databases this way. The video files go in object storage, never in the database, the same split the primer's <a href=\"/learn/design-pastebin\">Pastebin design</a> makes for paste contents.</p>","headings":[{"id":"api-and-data-model","title":"API and data model","depth":3}]},{"id":"high-level-design","title":"High-level design","html":"<h3 id=\"high-level-design\">High-level design</h3>\n<figure class=\"diagram\" data-text=\"Creator app <-> Upload API (create, complete); Creator app -> Raw uploads (object store) (PUT parts); Upload API -> Metadata DB; Raw uploads (object store) -> Workflow scheduler (upload done); Workflow scheduler -> Metadata DB (status); Workflow scheduler -> Task queues; Task queues -> Workers; Workers -> Segments and manifests\"><div class=\"diagram-scroll\"><svg class=\"dg\" viewBox=\"0 -8 800 285\" width=\"800\" role=\"img\" aria-label=\"Upload and processing path\"><defs><marker id=\"dg-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path class=\"dg-arrowhead\" d=\"M0,1 L9,5 L0,9 z\"></path></marker></defs><line class=\"dg-edge\" x1=\"153\" y1=\"42\" x2=\"167\" y2=\"42\" marker-end=\"url(#dg-arrow)\" marker-start=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"125\" y1=\"69\" x2=\"180\" y2=\"102\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"311\" y1=\"42\" x2=\"327\" y2=\"42\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge dg-dashed\" x1=\"311\" y1=\"138\" x2=\"327\" y2=\"138\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"400\" y1=\"109\" x2=\"400\" y2=\"76\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"471\" y1=\"138\" x2=\"487\" y2=\"138\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"631\" y1=\"138\" x2=\"647\" y2=\"138\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"720\" y1=\"165\" x2=\"720\" y2=\"198\" marker-end=\"url(#dg-arrow)\"></line><rect class=\"dg-node dg-client\" x=\"12\" y=\"18\" width=\"136\" height=\"48\" rx=\"24\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"80\" y=\"46.5\">Creator app</tspan></text><rect class=\"dg-node dg-box\" x=\"172\" y=\"18\" width=\"136\" height=\"48\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"240\" y=\"46.5\">Upload API</tspan></text><path class=\"dg-node dg-store\" d=\"M332,20 a68,7 0 0,1 136,0 v44 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M332,20 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"400\" y=\"49.5\">Metadata DB</tspan></text><path class=\"dg-node dg-store\" d=\"M172,114 a68,7 0 0,1 136,0 v48 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M172,114 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"240\" y=\"137.5\">Raw uploads</tspan><tspan x=\"240\" y=\"153.5\">(object store)</tspan></text><rect class=\"dg-node dg-box\" x=\"332\" y=\"112\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"400\" y=\"134.5\">Workflow</tspan><tspan x=\"400\" y=\"150.5\">scheduler</tspan></text><rect class=\"dg-node dg-queue\" x=\"492\" y=\"114\" width=\"136\" height=\"48\" rx=\"6\"></rect><line class=\"dg-line\" x1=\"612\" y1=\"122\" x2=\"612\" y2=\"154\"></line><line class=\"dg-line\" x1=\"602\" y1=\"122\" x2=\"602\" y2=\"154\"></line><line class=\"dg-line\" x1=\"592\" y1=\"122\" x2=\"592\" y2=\"154\"></line><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"546\" y=\"142.5\">Task queues</tspan></text><rect class=\"dg-node dg-box\" x=\"652\" y=\"114\" width=\"136\" height=\"48\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"720\" y=\"142.5\">Workers</tspan></text><path class=\"dg-node dg-store\" d=\"M652,210 a68,7 0 0,1 136,0 v48 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M652,210 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"720\" y=\"233.5\">Segments and</tspan><tspan x=\"720\" y=\"249.5\">manifests</tspan></text><rect class=\"dg-edge-bg\" x=\"105.4\" y=\"-4\" width=\"109.2\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"160\" y=\"9\" text-anchor=\"middle\">create, complete</text><rect class=\"dg-edge-bg\" x=\"119.6\" y=\"76.5\" width=\"65.8\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"152.5\" y=\"89.5\" text-anchor=\"middle\">PUT parts</text><rect class=\"dg-edge-bg\" x=\"279.9\" y=\"85\" width=\"78.2\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"319\" y=\"98\" text-anchor=\"middle\">upload done</text><rect class=\"dg-edge-bg\" x=\"408\" y=\"83.5\" width=\"47.2\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"431.6\" y=\"96.5\" text-anchor=\"middle\">status</text></svg></div><figcaption>Upload and processing path</figcaption></figure>\n<figure class=\"diagram\" data-text=\"Player -> CDN edge (segments); CDN edge -> Regional cache (miss); Regional cache -> Origin (object store) (miss); Player -> Playback API (play); Playback API -> Metadata and cache\"><div class=\"diagram-scroll\"><svg class=\"dg\" viewBox=\"0 -15 640 196\" width=\"640\" role=\"img\" aria-label=\"Playback path\"><defs><marker id=\"dg-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path class=\"dg-arrowhead\" d=\"M0,1 L9,5 L0,9 z\"></path></marker></defs><line class=\"dg-edge\" x1=\"151\" y1=\"42\" x2=\"167\" y2=\"42\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"311\" y1=\"42\" x2=\"327\" y2=\"42\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"471\" y1=\"42\" x2=\"487\" y2=\"42\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"125\" y1=\"69\" x2=\"191.7\" y2=\"109\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"311\" y1=\"138\" x2=\"327\" y2=\"138\" marker-end=\"url(#dg-arrow)\"></line><rect class=\"dg-node dg-client\" x=\"12\" y=\"18\" width=\"136\" height=\"48\" rx=\"24\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"80\" y=\"46.5\">Player</tspan></text><rect class=\"dg-node dg-box\" x=\"172\" y=\"18\" width=\"136\" height=\"48\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"240\" y=\"46.5\">CDN edge</tspan></text><rect class=\"dg-node dg-box\" x=\"332\" y=\"16\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"400\" y=\"38.5\">Regional</tspan><tspan x=\"400\" y=\"54.5\">cache</tspan></text><path class=\"dg-node dg-store\" d=\"M492,18 a68,7 0 0,1 136,0 v48 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M492,18 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"560\" y=\"41.5\">Origin</tspan><tspan x=\"560\" y=\"57.5\">(object store)</tspan></text><rect class=\"dg-node dg-box\" x=\"172\" y=\"114\" width=\"136\" height=\"48\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"240\" y=\"142.5\">Playback API</tspan></text><path class=\"dg-node dg-store\" d=\"M332,114 a68,7 0 0,1 136,0 v48 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M332,114 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"400\" y=\"137.5\">Metadata</tspan><tspan x=\"400\" y=\"153.5\">and cache</tspan></text><rect class=\"dg-edge-bg\" x=\"129.2\" y=\"-4\" width=\"59.6\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"159\" y=\"9\" text-anchor=\"middle\">segments</text><rect class=\"dg-edge-bg\" x=\"301.6\" y=\"-6\" width=\"34.8\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"319\" y=\"7\" text-anchor=\"middle\">miss</text><rect class=\"dg-edge-bg\" x=\"461.6\" y=\"-11\" width=\"34.8\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"479\" y=\"2\" text-anchor=\"middle\">miss</text><rect class=\"dg-edge-bg\" x=\"140.9\" y=\"80\" width=\"34.8\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"158.3\" y=\"93\" text-anchor=\"middle\">play</text></svg></div><figcaption>Playback path</figcaption></figure>\n<h4 id=\"uploads-with-pre-signed-urls\">Uploads with pre-signed URLs</h4>\n<p>Pushing 600 MB through your API servers wastes their bandwidth and ties up connections for minutes. Instead:</p>\n<ol>\n<li>The client calls <code>POST /videos</code>. The API creates a <code>videos</code> row with status <code>uploading</code>, starts a multipart upload in object storage, and returns <strong>pre-signed URLs</strong>, one per part. A pre-signed URL is a link signed with the service's credentials that allows exactly one kind of operation (here, a PUT to one part of one object) until it expires. It isn't single-use: anyone holding it can repeat that operation until then.</li>\n<li>The client splits the file into parts and uploads them directly to object storage, several at a time.</li>\n<li>If the connection drops, the client retries only the parts that failed. If the app restarts, it asks the API which parts already arrived and resumes from there. That makes the upload <strong>resumable</strong>.</li>\n<li>The client calls <code>complete</code> with the list of parts. Object storage stitches them into one object and emits an event, which starts processing.</li>\n</ol>\n<p>Part sizes depend on the store. Amazon S3, for example, requires parts of at least 5 MiB except the last, and allows up to 10,000 parts per upload. Choose part sizes so a large file stays well under the part limit and a failed part is cheap to retry. If you'd rather not depend on one vendor's multipart API, the open tus protocol defines resumable uploads over plain HTTP.</p>\n<p>The trade-off is that the API no longer sees the bytes, so validation happens afterward in the pipeline. Treat every upload as untrusted until it has been probed.</p>","headings":[{"id":"high-level-design","title":"High-level design","depth":3},{"id":"uploads-with-pre-signed-urls","title":"Uploads with pre-signed URLs","depth":4}]},{"id":"the-transcoding-pipeline","title":"The transcoding pipeline","html":"<h3 id=\"the-transcoding-pipeline\">The transcoding pipeline</h3>\n<p>A source file arrives in whatever format the creator's camera or editor produced. The pipeline turns it into a set of renditions every player can handle, plus thumbnails and captions. It's naturally a <strong>directed acyclic graph</strong> of tasks:</p>\n<figure class=\"diagram\" data-text=\"Probe and validate -> Split and encode chunks; Split and encode chunks -> Package HLS / DASH; Package HLS / DASH -> Publish; Probe and validate -> Thumbnails, captions; Thumbnails, captions -> Publish; Probe and validate -> Safety scan; Safety scan -> Publish\"><div class=\"diagram-scroll\"><svg class=\"dg\" viewBox=\"0 0 640 270\" width=\"640\" role=\"img\" aria-label=\"Tasks for one video; branches run in parallel\"><defs><marker id=\"dg-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path class=\"dg-arrowhead\" d=\"M0,1 L9,5 L0,9 z\"></path></marker></defs><line class=\"dg-edge\" x1=\"151\" y1=\"138\" x2=\"167\" y2=\"138\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"311\" y1=\"138\" x2=\"327\" y2=\"138\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"471\" y1=\"138\" x2=\"487\" y2=\"138\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"151\" y1=\"109.6\" x2=\"247\" y2=\"71.2\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"391\" y1=\"70.4\" x2=\"487.5\" y2=\"109\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"151\" y1=\"166.4\" x2=\"247.5\" y2=\"205\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"387.5\" y1=\"207\" x2=\"487.5\" y2=\"167\" marker-end=\"url(#dg-arrow)\"></line><rect class=\"dg-node dg-box\" x=\"12\" y=\"112\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"80\" y=\"134.5\">Probe and</tspan><tspan x=\"80\" y=\"150.5\">validate</tspan></text><rect class=\"dg-node dg-box\" x=\"172\" y=\"112\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"240\" y=\"134.5\">Split and</tspan><tspan x=\"240\" y=\"150.5\">encode chunks</tspan></text><rect class=\"dg-node dg-box\" x=\"332\" y=\"112\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"400\" y=\"134.5\">Package</tspan><tspan x=\"400\" y=\"150.5\">HLS / DASH</tspan></text><rect class=\"dg-node dg-box\" x=\"492\" y=\"114\" width=\"136\" height=\"48\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"560\" y=\"142.5\">Publish</tspan></text><rect class=\"dg-node dg-box\" x=\"252\" y=\"16\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"320\" y=\"38.5\">Thumbnails,</tspan><tspan x=\"320\" y=\"54.5\">captions</tspan></text><rect class=\"dg-node dg-box\" x=\"252\" y=\"210\" width=\"136\" height=\"48\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"320\" y=\"238.5\">Safety scan</tspan></text></svg></div><figcaption>Tasks for one video; branches run in parallel</figcaption></figure>\n<ul>\n<li><strong>Probe and validate:</strong> read the container, codecs, duration and resolution. Reject corrupt or unsupported files early, before spending compute on them.</li>\n<li><strong>Split:</strong> cut the video into chunks of a few seconds at keyframe boundaries. Each chunk can then be encoded independently on a different worker. A 2-hour film becomes thousands of small tasks that finish in minutes instead of one task that takes hours.</li>\n<li><strong>Encode:</strong> for each chunk, produce each rung of the bitrate ladder. All renditions must put keyframes at the same timestamps, so the player can switch between them at any segment boundary.</li>\n<li><strong>Package:</strong> join the encoded chunks into streaming segments and write the manifests.</li>\n<li><strong>Side tasks:</strong> thumbnails, speech-to-text captions and safety checks run in parallel with encoding, because they only depend on the probed source.</li>\n<li><strong>Publish:</strong> flip the status to <code>ready</code> and make the manifest URL live.</li>\n</ul>\n<p>How to run it:</p>\n<ul>\n<li>A <strong>workflow scheduler</strong> holds each video's DAG and enqueues a task when all its inputs are done. Workers pull tasks from <a href=\"/learn/asynchronism#task-queues\">task queues</a>, process them, write outputs to object storage and report completion. Off-the-shelf engines (Temporal, AWS Step Functions, Airflow) or a hand-built scheduler on a queue all fit.</li>\n<li><strong>Tasks are idempotent.</strong> A task writes its output to a deterministic path, so a retry after a worker crash overwrites the same file instead of creating a duplicate. Queues deliver at least once, so this matters.</li>\n<li><strong>Separate queues by work type and priority.</strong> Encoding needs CPU or GPU machines, captions may need GPUs, and thumbnails are cheap. Short videos and paying creators can go in a higher-priority queue. When a queue grows, scale its workers, and apply <a href=\"/learn/asynchronism#back-pressure\">back pressure</a> to uploads only as a last resort.</li>\n<li><strong>Publish early.</strong> Encode the low and medium rungs first and mark the video watchable as soon as they're packaged. The 1080p and 4K rungs fill in later.</li>\n</ul>","headings":[{"id":"the-transcoding-pipeline","title":"The transcoding pipeline","depth":3}]},{"id":"adaptive-bitrate-streaming","title":"Adaptive bitrate streaming","html":"<h3 id=\"adaptive-bitrate-streaming\">Adaptive bitrate streaming</h3>\n<p>A viewer's bandwidth changes from second to second. Adaptive bitrate (ABR) streaming handles this by encoding the video at several quality levels, cutting each into short segments, and letting the player choose a level for each segment.</p>\n<p>An example ladder (assumed values; real ladders vary by content):</p>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n<div class=\"table-wrap\"><table><thead><tr><th>Rendition</th><th>Resolution</th><th>Video bitrate</th></tr></thead><tbody><tr><td>240p</td><td>426×240</td><td>0.4 Mbps</td></tr><tr><td>360p</td><td>640×360</td><td>0.8 Mbps</td></tr><tr><td>480p</td><td>854×480</td><td>1.4 Mbps</td></tr><tr><td>720p</td><td>1280×720</td><td>2.8 Mbps</td></tr><tr><td>1080p</td><td>1920×1080</td><td>5.0 Mbps</td></tr></tbody></table></div>\n<p>That ladder adds up to 10.4 Mbps, which is where the \"about 10 Mbps of output\" in the estimate came from.</p>\n<p>The two main formats are <strong>HLS</strong> (HTTP Live Streaming, from Apple, published as RFC 8216) and <strong>MPEG-DASH</strong> (an ISO standard). Both work the same way:</p>\n<ul>\n<li>Each rendition is a sequence of segments, typically 2 to 6 seconds each.</li>\n<li>A <strong>media playlist</strong> (HLS) or a section of the <strong>MPD</strong> (DASH) lists one rendition's segments in order.</li>\n<li>A <strong>master playlist</strong> (HLS) or the MPD lists the renditions with their bandwidth and resolution.</li>\n</ul>\n<p>A trimmed HLS master playlist:</p>\n<pre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span>#EXTM3U</span></span>\n<span class=\"line\"><span>#EXT-X-STREAM-INF:BANDWIDTH=900000,RESOLUTION=640x360</span></span>\n<span class=\"line\"><span>360p/index.m3u8</span></span>\n<span class=\"line\"><span>#EXT-X-STREAM-INF:BANDWIDTH=3100000,RESOLUTION=1280x720</span></span>\n<span class=\"line\"><span>720p/index.m3u8</span></span>\n<span class=\"line\"><span>#EXT-X-STREAM-INF:BANDWIDTH=5500000,RESOLUTION=1920x1080</span></span>\n<span class=\"line\"><span>1080p/index.m3u8</span></span></code></pre>\n<p><code>BANDWIDTH</code> is the peak bitrate in bits per second, including audio and container overhead, which is why it's a little above the video bitrate in the table.</p>\n<p>The player downloads the master playlist, picks a starting rendition, and then fetches segments over ordinary HTTP. Before each segment, it looks at its recent download throughput and how many seconds of video are buffered, and moves up or down the ladder. Because everything is plain HTTP GETs of immutable files, every segment is cacheable by any CDN, which is the main reason HTTP-based streaming displaced older stateful protocols such as RTMP for delivery.</p>\n<p>Practical notes:</p>\n<ul>\n<li><strong>CMAF</strong> (fragmented MP4 segments) lets one set of segment files serve both HLS and DASH players. Without it you might store each rendition twice (MPEG-TS segments for older HLS players, fMP4 for DASH), doubling storage and splitting cache space between two copies.</li>\n<li><strong>Shorter segments</strong> let the player adapt faster and start sooner, at the cost of more requests and slightly worse compression.</li>\n<li><strong>Per-title encoding.</strong> A static lecture needs far fewer bits than a fast sports clip to look good. Netflix has written about choosing a ladder per title from its complexity. It costs more analysis per video and saves bandwidth on every view.</li>\n</ul>","headings":[{"id":"adaptive-bitrate-streaming","title":"Adaptive bitrate streaming","depth":3}]},{"id":"scaling-and-bottlenecks","title":"Scaling and bottlenecks","html":"<h3 id=\"scaling-and-bottlenecks\">Scaling and bottlenecks</h3>\n<h4 id=\"cdn-delivery-and-cost\">CDN delivery and cost</h4>\n<p>At around 9 Tbps peak, nearly all playback traffic must come from CDN caches, and egress is the largest bill. To make the cost concrete, suppose a blended delivery price of $0.005 per GB. That number is made up for the arithmetic; real contracts vary widely with volume. Then 40 PB a day is 40 × 10⁶ GB × $0.005 = <strong>$200,000 a day</strong>. Every percent of bitrate you save is worth $2,000 a day.</p>\n<p>Popularity is heavily skewed: a small share of videos gets most of the views, and a long tail is rarely watched. That changes how you treat each:</p>\n<ul>\n<li><strong>Popular videos</strong> stay hot in edge caches with very high hit rates. Spend extra encoding effort on them: more efficient codecs such as VP9, HEVC or AV1, and per-title ladders. The encoding cost is paid once and the savings repeat on every view.</li>\n<li><strong>Long-tail videos</strong> mostly miss at the edge. A tiered setup (edge, then regional cache, then origin) keeps origin load manageable. You can also skip some renditions or codecs until a video proves popular, and transcode them on demand. Store rarely watched sources and renditions in a cheaper storage class; Facebook's f4 paper describes moving older, less-read files to erasure-coded \"warm\" storage to cut storage overhead.</li>\n<li><strong>Pre-positioning.</strong> For predictable spikes, like a big premiere, push content to edges ahead of time instead of letting the first viewers fault it in. See <a href=\"/learn/content-delivery-network#push-cdns\">push vs pull CDNs</a>.</li>\n<li><strong>Owning the edge.</strong> At very large scale, platforms place their own cache servers inside ISP networks; Netflix's Open Connect program is the best-known example. It trades capital and operational cost for lower per-byte cost and better performance.</li>\n</ul>\n<h4 id=\"other-bottlenecks\">Other bottlenecks</h4>\n<ul>\n<li><strong>Metadata reads</strong> for a popular video page are cached aggressively; the metadata DB mostly sees writes and cache misses. See <a href=\"/learn/cache#application-caching\">cache</a>.</li>\n<li><strong>View counts.</strong> At 10 views per viewer a day (an assumption), that's 400 million views a day, about 4,600 a second on average, concentrated on a few hot videos. Incrementing a row per view makes those rows a bottleneck. Instead, send view events to a stream, filter out duplicates and bots, aggregate per video per minute, and add the aggregates to the stored count. The displayed count lags by minutes, and nobody minds. <a href=\"/learn/event-streaming\">Event streaming</a> covers the pipeline.</li>\n<li><strong>Transcoding capacity</strong> follows upload volume, which peaks on weekends and around events. Workers that scale with queue depth, and spot or preemptible machines for low-priority work, keep costs down.</li>\n<li><strong>Hot origin objects.</strong> If a video goes viral before edges have it, many edges miss at once. Request collapsing in the regional tier (one origin fetch per object, shared by waiting requests) protects origin.</li>\n</ul>\n<h4 id=\"drm-and-safety\">DRM and safety</h4>\n<p>Mention both and keep moving unless the interviewer digs in:</p>\n<ul>\n<li><strong>DRM:</strong> segments are encrypted (the Common Encryption standard can let one encrypted file work with several DRM systems), and players get keys from a license server after an entitlement check. Widevine, FairPlay and PlayReady cover most devices. The pipeline gains an encryption step, and <code>GET /play</code> returns a license URL.</li>\n<li><strong>Safety:</strong> the pipeline runs hash matching against known illegal content, classifiers for policy violations, and copyright fingerprinting before or shortly after publishing. Decide which checks block publishing and which run after; that's a latency vs risk trade-off.</li>\n</ul>","headings":[{"id":"scaling-and-bottlenecks","title":"Scaling and bottlenecks","depth":3},{"id":"cdn-delivery-and-cost","title":"CDN delivery and cost","depth":4},{"id":"other-bottlenecks","title":"Other bottlenecks","depth":4},{"id":"drm-and-safety","title":"DRM and safety","depth":4}]},{"id":"trade-offs-to-discuss","title":"Trade-offs to discuss","html":"<h3 id=\"trade-offs-to-discuss\">Trade-offs to discuss</h3>\n<ul>\n<li><strong>More renditions and codecs vs storage and compute.</strong> Each extra rung or codec improves playback for some viewers and multiplies storage. Popularity-based encoding puts the cost where it pays back.</li>\n<li><strong>Segment length:</strong> faster adaptation and startup vs more requests and slightly larger files.</li>\n<li><strong>Publish speed vs completeness:</strong> publishing after the low rungs makes videos available sooner, while a 4K viewer briefly sees lower quality.</li>\n<li><strong>Pre-signed direct uploads vs proxying through the API:</strong> far less load on your servers vs less control during the upload, so validation moves into the pipeline.</li>\n<li><strong>Third-party CDN vs your own edge:</strong> pay per byte and move fast, or invest in your own infrastructure once volume justifies it.</li>\n<li><strong>Exact vs approximate view counts:</strong> exact counting is expensive at hot keys; batched, slightly delayed counts are usually fine.</li>\n</ul>","headings":[{"id":"trade-offs-to-discuss","title":"Trade-offs to discuss","depth":3}]},{"id":"check-yourself","title":"Check yourself","html":"<h3 id=\"check-yourself\">Check yourself</h3>\n<details>\n<summary>A creator on hotel Wi-Fi uploads a 4 GB file and the connection drops at 3.1 GB. What happens in your design?</summary>\n<p>The file was being sent as many independent parts to pre-signed URLs, so the parts that completed are already stored. When the app reconnects, it asks the API which parts have arrived, requests fresh pre-signed URLs if the old ones expired, and uploads only the missing parts. Then it calls <code>complete</code>. Nothing is re-sent from the beginning, and the API servers never carried the bytes.</p>\n</details>\n<details>\n<summary>Why must every rendition have keyframes at the same timestamps?</summary>\n<p>The player switches renditions at segment boundaries, and each segment must start with a keyframe so it can be decoded without earlier data. If the 480p and 720p segments started at different times, switching would leave a gap or overlap and a visible glitch. Aligned keyframes, set during encoding, make segment N of every rendition cover the same time span.</p>\n</details>\n<details>\n<summary>Your encoding team says AV1 cuts bitrate by a meaningful amount at the same quality but costs several times more compute to encode. Should you encode everything in AV1?</summary>\n<p>Not everything. The encoding cost is paid once per video, while the bandwidth savings are paid back on every view, so it pays off only for videos that get enough views, and only for viewers whose devices can decode AV1. A common policy is to encode a widely supported codec for everything and add the more efficient one once a video crosses a popularity threshold. Use the estimate to find roughly where the break-even point is.</p>\n</details>\n<details>\n<summary>A worker crashes halfway through encoding chunk 37 of a film. How does the pipeline recover without corrupting the output?</summary>\n<p>The task was never marked complete, so after its visibility timeout or lease expires, the queue hands it to another worker. Because the task writes to a deterministic path (for example <code>video/720p/chunk-37</code>), the retry overwrites any partial output instead of creating a second copy. The packaging task only starts when the scheduler sees all chunks complete, so it never reads a half-written chunk.</p>\n</details>\n<details>\n<summary>Long-tail videos make up most of your catalog but few of your views. Name two ways to cut what they cost you.</summary>\n<p>Store them more cheaply: move older, rarely watched sources and renditions to a cheaper storage class or erasure-coded storage. And don't produce what nobody watches: skip high rungs or extra codecs for new videos until they get views, and transcode on demand if someone does ask. Both trade a little latency for a rare viewer against a lot of storage and compute.</p>\n</details>\n<h3 id=\"further-reading\">Further reading</h3>\n<ul>\n<li><a href=\"https://www.rfc-editor.org/rfc/rfc8216\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 8216: HTTP Live Streaming</a>: the HLS playlist format, including what <code>BANDWIDTH</code> means.</li>\n<li><a href=\"https://developer.apple.com/streaming/\" target=\"_blank\" rel=\"noopener noreferrer\">Apple HTTP Live Streaming</a>: Apple's HLS tools and authoring guidance.</li>\n<li><a href=\"https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html\" target=\"_blank\" rel=\"noopener noreferrer\">Uploading and copying objects using multipart upload</a> (Amazon S3): part limits, retries and completing uploads.</li>\n<li><a href=\"https://tus.io/protocols/resumable-upload\" target=\"_blank\" rel=\"noopener noreferrer\">tus resumable upload protocol</a>: an open protocol for resumable uploads over HTTP.</li>\n<li><a href=\"https://netflixtechblog.com/per-title-encode-optimization-7e99442b62a2\" target=\"_blank\" rel=\"noopener noreferrer\">Per-title encode optimization</a> (Netflix): why one bitrate ladder doesn't fit every video.</li>\n<li><a href=\"https://openconnect.netflix.com/\" target=\"_blank\" rel=\"noopener noreferrer\">Netflix Open Connect</a>: a streaming company's own CDN, placed inside ISP networks.</li>\n<li><a href=\"https://www.usenix.org/conference/osdi14/technical-sessions/presentation/muralidhar\" target=\"_blank\" rel=\"noopener noreferrer\">f4: Facebook's warm BLOB storage system</a> (OSDI 2014): storing rarely read media cheaply.</li>\n<li>Alex Xu, <em>System Design Interview – An Insider's Guide</em> (2020), \"Design YouTube\".</li>\n<li><a href=\"https://systemdesignschool.io/primer\" target=\"_blank\" rel=\"noopener noreferrer\">System Design School primer</a>: \"Object Storage\", \"CDN\" and \"Buffer with Message Queues\".</li>\n</ul>","headings":[{"id":"check-yourself","title":"Check yourself","depth":3},{"id":"further-reading","title":"Further reading","depth":3}]}],"sourceFiles":[]}