Designing YouTube: Video Upload, Transcoding, and Streaming at Scale
Analyze how to design a video-sharing platform like YouTube. Cover resumable uploads, async transcoding pipelines, adaptive bitrate streaming, CDN delivery, and approximate view counters.
Why Study YouTube?
YouTube ingests over 500 hours of video every minute from creators worldwide and serves over 2 billion logged-in users monthly. Unlike a licensed-content platform, YouTube must accept, process, and safely publish anything anyone uploads - from a 30-second phone clip to a multi-hour 4K livestream recording. That upload-side problem, not just playback, is what makes YouTube a distinct system design case study.
Approach: Interviewers rarely ask you to "design YouTube" wholesale - they ask you to design the upload and transcoding pipeline or the playback and view-counting system. Treat this as two connected sub-systems and be explicit about which one you're solving.
Requirements Analysis
Functional Requirements
- Video Upload: Creators upload videos up to several hours long and tens of gigabytes in size
- Transcoding: Every upload is converted into multiple resolutions and bitrates
- Playback: Viewers stream video that adapts to their device and network conditions
- View Counting: Track how many times a video has been watched
- Metadata Management: Titles, descriptions, thumbnails, captions
- Content Availability: Videos become watchable shortly after upload completes
Non-Functional Requirements
| Requirement | Target |
|---|---|
| Upload reliability | Multi-GB uploads must survive network interruptions |
| Time-to-first-frame | < 2 seconds to start playback |
| Transcoding latency | Standard-definition renditions ready within minutes |
| Concurrency | Millions of simultaneous streams globally |
| Adaptive | Quality adjusts smoothly to available bandwidth |
| Durability | Uploaded source video is never lost |
High-Level Architecture
Where this differs from Netflix: Netflix's catalog is licensed and pre-processed by studios before it ever reaches Netflix's pipeline - there's no public upload path. YouTube's defining engineering challenge is the ingestion side: accepting arbitrary, unpredictable, massive files from anyone and turning them into streamable video reliably. Everything from here focuses on that upload-to-playback journey rather than CDN/recommendation internals already covered in the Netflix case study.
Resumable, Chunked Upload
Why a Single HTTP Request Doesn't Work
A 4K video can easily be 20-50 GB. Uploading that as one HTTP PUT/POST has real problems at scale:
- Network interruptions are common. Mobile networks, hotel WiFi, and long-lived TCP connections fail. A single dropped connection at byte 19GB means restarting from zero.
- Server-side buffering. A naive server that buffers the whole request body before processing needs enormous memory/disk per concurrent upload.
- No progress visibility. Users have no way to resume or see partial progress with a monolithic request.
- Timeouts. Load balancers and proxies often cap request duration; a multi-hour upload on a slow connection can exceed those limits.
Chunked Upload Protocol
The client splits the file into fixed-size chunks (commonly 8-16 MB), uploads them independently, and each chunk is acknowledged and persisted before the next is sent. If the connection drops, the client asks the server which chunks it already has and resumes from there instead of restarting. This is the same principle behind the resumable upload protocol YouTube's own API documents and behind the TUS open protocol.
Chunked Upload Design Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Chunk size | 8-16 MB | Balances retry cost vs per-chunk request overhead |
| Chunk storage | Object store (S3-compatible) with multipart upload API | Native support for parallel, resumable parts |
| Integrity check | Per-chunk checksum + final file hash | Detects corruption before wasting transcoding compute |
| Upload session state | Stored server-side, keyed by upload_id | Lets the client resume from any device/tab |
| Parallelism | Multiple chunks in flight (client-controlled) | Saturates available bandwidth on fast connections |
Interview signal: Naming "resumability" alone isn't enough - explain why chunking enables it (independent, acknowledged units of work) and how the client discovers where to resume (a status endpoint backed by server-side session state, not just client memory).
Async Transcoding Pipeline
Why Transcoding Must Be Asynchronous
A single uploaded video needs to become many output files: several resolutions (144p through 4K), several bitrates per resolution, multiple codecs (H.264 for compatibility, VP9/AV1 for efficiency), and audio tracks. Transcoding a long video can take longer than the upload itself. Doing this synchronously would mean holding an HTTP connection open for many minutes and tying up upload-tier capacity with CPU-bound work - two very different scaling profiles that shouldn't share infrastructure.
The Pipeline
Each resolution is transcoded by an independent worker pulling from a queue, so a slow 4K job never blocks a fast 144p job, and the system can prioritize low-resolution renditions first so the video becomes watchable (on smaller screens or slow connections) before every rendition finishes.
Transcoding Job Priorities
| Rendition | Priority | Typical Availability After Upload |
|---|---|---|
| 360p/480p | Highest | Minutes |
| 720p | High | Minutes to tens of minutes |
| 1080p | Medium | Tens of minutes |
| 4K/HDR | Low | Can take hours for long videos |
| Captions/audio tracks | Parallel, independent | Minutes |
Why prioritize low resolutions first? Most early views come from mobile or lower-bandwidth connections, and getting some watchable rendition live fast matters more for engagement than having every rendition ready simultaneously. The video is marked "processing" and playable in a lower quality while higher renditions continue in the background.
Adaptive Bitrate Streaming (HLS/DASH)
How the Player Chooses Quality
Adaptive bitrate (ABR) streaming works by splitting each rendition into short segments (typically 2-10 seconds) and publishing a manifest file that lists every available rendition and where its segments live. The player - not the server - decides which rendition to request for each segment, based on measured download throughput and buffer health.
HLS vs DASH
| Aspect | HLS | DASH |
|---|---|---|
| Origin | Apple | MPEG standard |
| Manifest format | .m3u8 (playlist) | .mpd (XML) |
| Native support | iOS/Safari (built-in) | Android/most browsers (via player library) |
| Segment format | .ts or fragmented MP4 | Fragmented MP4 |
| Codec flexibility | Historically H.264-centric | Codec-agnostic |
YouTube supports both, serving the appropriate manifest based on the requesting device and player. Both formats express the same idea: a hierarchy of manifests (master → per-rendition playlist) pointing at small, independently-fetchable segments.
Why segments instead of one continuous stream per quality? Segmenting lets the player switch renditions between segments without re-establishing a connection or re-buffering from zero - it just starts requesting the next segment from a different rendition's playlist. It also lets CDN edges cache and serve segments as ordinary immutable HTTP objects.
CDN Delivery and Origin Shielding
Popular segments (viral videos) get requested by thousands of edge locations simultaneously. Without an origin shield, every edge cache miss goes straight to origin storage, multiplying load on the same hot object. An origin shield sits between edges and origin: it's a smaller set of regional caches that de-duplicate concurrent misses (many edges requesting the same segment collapse into a single origin fetch) and absorb the bulk of repeat traffic, so origin storage sees a small fraction of total request volume.
| Content Type | Cache Tier | TTL |
|---|---|---|
| Segments of popular/trending videos | Edge + Shield | Days (immutable once published) |
| Segments of long-tail videos | Shield only, fetched on demand | Hours, evicted under pressure |
| Manifests | Edge, short TTL | Minutes (renditions can be added later) |
| Thumbnails | Edge | Days |
Why segments are cache-friendly: Once a rendition is transcoded, its segments never change - they're content-addressable and immutable, which makes them ideal for aggressive, long-TTL caching. Only the manifest (which lists what's available) needs a short TTL, since new renditions can be appended as they finish transcoding.
Database Schema
Videos Table
CREATE TABLE videos (
id BIGINT PRIMARY KEY,
uploader_id BIGINT NOT NULL,
title VARCHAR(200) NOT NULL,
description TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'processing',
-- processing | ready | failed | removed
duration_seconds INT,
source_file_key VARCHAR(500) NOT NULL,
thumbnail_url VARCHAR(500),
visibility VARCHAR(20) NOT NULL DEFAULT 'private',
-- private | unlisted | public
created_at TIMESTAMP DEFAULT NOW(),
published_at TIMESTAMP
);
CREATE INDEX idx_uploader_id ON videos(uploader_id, created_at DESC);
CREATE INDEX idx_status ON videos(status);Renditions Table
CREATE TABLE renditions (
id BIGINT PRIMARY KEY,
video_id BIGINT NOT NULL REFERENCES videos(id),
resolution VARCHAR(10) NOT NULL, -- e.g. '1080p'
codec VARCHAR(20) NOT NULL, -- e.g. 'h264', 'vp9', 'av1'
bitrate_kbps INT NOT NULL,
manifest_path VARCHAR(500) NOT NULL,
segment_prefix VARCHAR(500) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'queued',
-- queued | processing | ready | failed
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_video_renditions ON renditions(video_id, status);
CREATE UNIQUE INDEX idx_video_res_codec ON renditions(video_id, resolution, codec);View Counts Table
CREATE TABLE view_counts (
video_id BIGINT PRIMARY KEY REFERENCES videos(id),
approximate_views BIGINT NOT NULL DEFAULT 0,
last_batch_applied_at TIMESTAMP,
updated_at TIMESTAMP DEFAULT NOW()
);
-- Raw view events land here first, batched and aggregated later
CREATE TABLE view_events_buffer (
id BIGINT PRIMARY KEY,
video_id BIGINT NOT NULL,
viewer_session_hash VARCHAR(64) NOT NULL,
watched_seconds INT NOT NULL,
event_time TIMESTAMP NOT NULL
);
CREATE INDEX idx_events_video_time ON view_events_buffer(video_id, event_time);Why separate renditions from videos? A video's playability is really the union of its ready renditions. Modeling renditions as their own rows lets the manifest service publish "what's ready now" incrementally as each transcoding job finishes, instead of waiting for one big all-or-nothing update.
The Approximate View Counter Problem
Why You Can't Just UPDATE ... SET views = views + 1
A synchronous increment per view sounds simple, but at YouTube's scale it breaks down fast:
- Write contention. A viral video can be watched thousands of times per second. Every view hitting the same row as a synchronous
UPDATEcreates a hot-row lock contention bottleneck. - Fraud and replay. Naive counting is trivially gameable - refreshing a page, bot traffic, or a single viewer replaying a video shouldn't all count as new views. A raw increment can't apply "does this count as a real view" logic (e.g., minimum watch duration) without extra reads on the hot path.
- Playback latency coupling. If starting playback requires a synchronous write to a view counter, video start time now depends on database write latency - exactly the kind of coupling adaptive streaming and CDN delivery are designed to avoid.
Batched, Eventually-Consistent Counting
Instead of incrementing on every view, the player emits a lightweight, asynchronous event once a view is judged "real" (e.g., a minimum watch threshold is crossed). Events land in a stream/log rather than the database directly. A batch aggregation job periodically deduplicates and validates events, then applies a single bulk increment per video per batch window - collapsing potentially thousands of individual events into one write.
This is deliberately similar in spirit to Instagram's like/comment counters (also asynchronous and eventually consistent) but distinct in mechanism: Instagram's counters update relatively quickly per-action against a social graph, while view counters additionally need watch-time validation and heavier anti-fraud filtering before a "view" is even accepted into the aggregation batch - so the displayed number can lag the true count by design, and briefly "hovers" during viral spikes rather than climbing in real time.
| Approach | Consistency | Write Load | Fraud Resistance |
|---|---|---|---|
| Synchronous per-view UPDATE | Strong | Extremely high, hot-row contention | None (needs extra logic) |
| Async event + batched aggregation | Eventual (seconds-minutes lag) | Low, amortized | Filtering happens before aggregation |
| In-memory counter + periodic flush | Eventual, fastest reads | Low | Needs separate validity pipeline |
Why the number sometimes "freezes": Viewers occasionally notice a viral video's view count pause and then jump. That's the batch aggregation window - and often a manual review step for anomalous spikes - not a bug. Trading real-time precision for write scalability and fraud resistance is the entire point of an approximate counter.
Scaling Challenges & Solutions
| Challenge | Solution |
|---|---|
| Multi-GB uploads over unreliable networks | Chunked, resumable upload protocol with server-side session state |
| Transcoding is CPU-heavy and slow | Async queue-driven workers, parallelized per resolution, lowest-resolution-first prioritization |
| Videos must be watchable before all renditions finish | Incremental manifest publishing as each rendition completes |
| Millions of concurrent streams with varying bandwidth | Segmented adaptive bitrate streaming (HLS/DASH), client-driven quality selection |
| Hot/viral content overwhelms origin storage | Multi-tier CDN with an origin shield layer to collapse duplicate misses |
| View counting at massive write volume | Async event ingestion + batched, deduplicated aggregation |
| Fraudulent/bot views inflating counts | Watch-time thresholds and validity filtering before aggregation |
| Long-tail videos rarely watched | Renditions cached on demand rather than pre-pushed to every edge |
Key Takeaways
- Chunk uploads, don't stream them as one request: Independent, acknowledged chunks make multi-gigabyte uploads resumable and memory-safe on the server.
- Decouple upload from transcoding: A queue between them lets CPU-bound transcoding scale independently of upload-tier capacity.
- Publish incrementally: Don't wait for every rendition - make the video watchable as soon as the lowest usable rendition is ready.
- Let the client choose quality: Adaptive bitrate streaming works because the player, closest to the actual network conditions, makes the switching decision - not the server.
- Shield your origin: A caching tier between edges and origin storage is what keeps a viral spike from taking down the storage layer.
- Approximate counters aren't a shortcut, they're a requirement: At sufficient write volume, eventual consistency for counters is the only design that scales - and it doubles as a natural point to filter fraud.
Interview tip: If asked to design YouTube, explicitly separate the upload/transcoding pipeline from the playback/CDN path in your diagram - they have opposite load profiles (bursty, large-payload writes vs. steady, small-payload reads) and should be reasoned about, scaled, and often even operated as distinct systems.
Follow-Up Questions to Consider
- How would you support live streaming instead of pre-recorded video, where segments must be generated and published in near real-time?
- How would you implement content moderation (copyright detection, policy violations) without delaying publish time for compliant creators?
- How would you handle re-transcoding an already-published video (e.g., adding a new codec like AV1 to old content) without downtime?
- How would you design closed captioning, including auto-generated captions for videos in many languages?
- How would you rate-limit or throttle a single uploader who publishes an unusually large volume of content in a short window?
Real YouTube trivia: YouTube processes video through Google's internal transcoding infrastructure and has historically pushed adoption of the VP9 and AV1 codecs specifically to cut its own outbound bandwidth costs, since AV1 can deliver similar quality at roughly half the bitrate of H.264. YouTube also popularized the "vp9/av1 for popular videos first" strategy - a video's transcoding ladder can keep expanding over time as it accumulates views, rather than being fixed permanently at upload.