This is the YouTube or Netflix question. It looks like a "store a file and send it back" problem, and that is exactly why it catches people. Storing the file is the easy part. Getting a 4K movie to a phone on a shaky train connection, a few hundred million times a day, without going bankrupt, is the actual question.
1. Problem Statement
Design a service where creators upload videos and viewers watch them on phones, laptops and TVs.
A creator uploads one big file. The service turns it into many smaller versions, stores them, and plays the right version for each viewer based on their screen and their internet speed. Viewers see a title, a thumbnail and a view count, and they can start playing within a couple of seconds and seek around freely.
Think of a print shop. The creator hands over one master copy. The shop makes small, medium and large prints, and each customer walks away with the size that fits their wall. The shop does the printing once, not once per customer.
2. Questions to Ask First
Say these out loud at the start of an interview. They shrink the problem and show you know where the hard parts are.
| Question | Assumption I will make |
|---|---|
| Video on demand or live? | On demand only. Live streaming is a different design, because you cannot prepare the segments ahead of time. |
| How many people use it? | 100 million viewers a day, 3 videos each. |
| How many uploads? | 500,000 videos a day. |
| How long and how big is a video? | About 8 minutes, and the raw file is about 500 MB. |
| Which devices? | Phones, browsers, smart TVs. So we need a format every device can play. |
| How fast must a new video appear? | Playable within a few minutes of upload finishing. |
| Is a view count allowed to be a bit stale? | Yes. A few seconds or even a minute behind is fine. |
3. Requirements
3.1 Functional Requirements
| Requirement | Detail |
|---|---|
| Upload | A creator uploads a large video file, and the upload can resume after a dropped connection |
| Process | The service creates several sizes and quality levels of the video |
| Play | A viewer plays the video. It starts fast and does not keep stopping to buffer |
| Adapt | Quality moves up and down on its own as the viewer's network changes |
| Seek | Jumping to minute 5 works instantly without downloading minutes 1 to 4 |
| Thumbnails | Every video has a picture to click on, and the player shows preview frames when scrubbing |
| Metadata | Title, description, owner, duration, visibility |
| View count | Each video shows how many times it was watched |
3.2 Non Functional Requirements
- Playback must start in under 2 seconds on a normal connection. People leave if the spinner stays longer.
- Playback must almost never stall. A blurry picture is fine, a frozen one is not.
- Reads dwarf writes. There are about 600 plays for every upload, so the whole design leans toward serving.
- No uploaded video is ever lost. A creator who loses a 2 hour recording will not come back.
- Highly available for viewers. A viewer should still be able to watch while parts of the system are broken.
- Cost matters as much as speed here. At this size the bandwidth bill is a design input, not an afterthought.
4. Back of the Envelope Numbers
Do these on the whiteboard, slowly, one small step at a time. The point is not the exact answer. The point is to find out which number is scary.
4.1 Traffic
Plays per day = 100M viewers x 3 videos = 300M
Seconds per day = 24 x 60 x 60 = 86,400
Plays per second = 300M / 86,400 ~ 3,500
Peak (3x average) ~ 10,000 plays per second
Uploads per day = 500,000
Uploads per second = 500,000 / 86,400 ~ 6
Plays per upload = 300M / 500K = 600Six uploads a second is nothing. Ten thousand plays a second is also fine as a count of requests. The scary number comes next.
4.2 Bytes Going Out
Average bitrate sent to viewers = 2.5 Mbps (phones lower, TVs higher)
Average time watched per play = 6 minutes = 360 seconds
Data per play = 2.5 Mbit/s x 360 s = 900 Mbit = 112 MB
Per day = 300M plays x 112 MB = 33,600 TB = 33.6 PB
Per second = 33.6 PB / 86,400 ~ 390 GB/s ~ 3.1 Tbps
Peak (3x) ~ 9 TbpsA good server network card does 10 Gbps. At 3.1 Tbps you would need over 300 servers doing nothing but pushing bytes at full speed, in one data centre, with the internet links to match. No company builds that in one place. This one number is the reason CDNs exist, and we will use it in the design.
4.3 Bytes Coming In
Raw uploads per day = 500,000 x 500 MB = 250 TB
Per second = 250 TB / 86,400 ~ 2.9 GB/s ~ 23 Gbps
Upload part size = 8 MB, so one 500 MB file = 500 / 8 ~ 63 partsViewers pull about 130 times more than creators push.
4.4 Storage
One video becomes several renditions. A rendition is one version of the video at one size and quality level. A typical set (people call it a ladder) looks like this:
| Rendition | Bitrate |
|---|---|
| 240p | 0.4 Mbps |
| 360p | 0.8 Mbps |
| 480p | 1.4 Mbps |
| 720p | 2.8 Mbps |
| 1080p | 5.0 Mbps |
| Total | 10.4 Mbps |
8 minutes = 480 seconds
All five renditions = 10.4 Mbit/s x 480 s = 4,992 Mbit = 624 MB ~ 600 MB
Keep the original too = 500 MB
Per video ~ 1.1 GB
Per day = 500,000 x 1.1 GB = 550 TB
Per year = 550 TB x 365 ~ 200,000 TB = 200 PBNow count the files, because object stores charge and slow down per file, not just per byte. Assume segments of 4 seconds (explained in the next section).
Segments per rendition = 480 s / 4 s = 120
Segments per video = 120 x 5 renditions = 600 files
Files per day = 500,000 x 600 = 300M new objectsThree hundred million new small files a day. Most object stores handle that, but it tells you to plan the key layout and not to list directories casually.
4.5 Metadata
Videos in 5 years = 500,000 x 365 x 5 ~ 900M rows x 1 KB ~ 900 GBThat fits on a handful of machines. We still split it up, but for read traffic, not for space.
4.6 Transcoding Compute
Rough rule: one CPU core encodes about as fast as the video plays, for the heavier sizes. Say the whole ladder for an 8 minute video costs about 40 core minutes of work.
Per day = 500,000 videos x 40 core minutes = 20M core minutes
Cores running all day = 20M / 1,440 minutes ~ 14,000 cores
Peak (3x) ~ 40,000 coresSo a fleet of about 14,000 cores on average. That is real money, and it is a good place for cheap interruptible machines, because transcoding work can be retried.
4.7 The Money
These are rough public price levels. Prices change, so keep the shape of the answer and not the digits.
Storage: about $20 per TB per month
After one year of uploads: 200,000 TB x $20 = $4M per month
Egress at normal cloud price: about $0.05 per GB
33.6 PB per day = 33,600,000 GB x $0.05 = $1.68M per DAY ~ $50M per month
Egress on a big CDN contract: about $0.005 per GB
33,600,000 GB x $0.005 = $168K per day ~ $5M per monthRead that again. At standard cloud prices, a month of bandwidth ($50M) costs about 12 times what a month of storing a whole year of uploads costs ($4M). Bandwidth is the bill. Storage is small beside it. That changes how you design: you spend effort to reduce bytes sent, not bytes stored.
One consequence you will want to mention. A newer codec such as AV1 makes files roughly 30 percent smaller at the same quality, but it costs several times more CPU to encode and some old devices cannot play it. Encoding it for every upload would waste money, because most videos are watched a handful of times. The smart move is to encode H.264 for everyone and add AV1 only for videos that get popular. A video watched 10 million times pays back the extra encoding many times over.
4.8 Cache Hit Rate
If 95 percent of the bytes come from CDN caches, the origin serves 5 percent of 33.6 PB, which is 1.68 PB a day, about 19 GB/s or 155 Gbps. Still big, but 20 times smaller. Going from 95 to 97 percent takes it down to about 93 Gbps, so every point of hit rate is worth real money.
5. High Level Design
First, the two ideas that shape every box on the diagram.
5.1 Chopping Video Into Segments (HLS and DASH)
You do not send a viewer one giant 600 MB file. You cut each rendition into short pieces, about 4 seconds each, called segments. Then you write a small text file called a playlist that lists the segments in order.
HLS (from Apple) and DASH (an open standard) are two ways of writing this down. The idea is identical. HLS playlists end in .m3u8, DASH ones end in .mpd. We will pick HLS, because every Apple device plays it natively, and browsers and Android play it through a small player library. Modern HLS can use the same segment files that DASH uses, so we would not store everything twice if we added DASH later.
There are two levels of playlist:
master.m3u8 "here are the versions I have"
240p/index.m3u8 "here are the 120 segments of the 240p version"
360p/index.m3u8
480p/index.m3u8
720p/index.m3u8
1080p/index.m3u8
seg_000.m4s 4 seconds of video
seg_001.m4s
...Think of a recipe card that says "step 1 is on page 4, step 2 is on page 5". The player reads the card and fetches the pages one after another. Because each page is a plain file, a plain web server or CDN can hand it out. No special streaming server is needed. This is the biggest simplification in the whole design.
Seeking also becomes easy. To jump to minute 5, the player works out which segment starts at 300 seconds (segment number 75) and fetches that one. It never touches the first 74.
5.2 Adaptive Bitrate Playback
The player is the smart part. While it plays, it keeps measuring how fast each segment downloaded. If a 4 second segment of 720p took 1 second to arrive, the network is comfortable, so it asks for 1080p next. If it took 5 seconds, which is slower than playing, it drops to 480p before the buffer runs out.
The server does nothing special. It does not even know the viewer is switching. All the decisions happen in the player, and all it needs is the same segment boundaries in every rendition so it can switch cleanly at a segment edge. This is called adaptive bitrate, or ABR.
I like this design because the hard, per-viewer decision is pushed out to the viewer's own device, and the servers just hand out static files.
5.3 The Diagram
5.4 Walking Through Each Box
API servers. They handle the small requests: create an upload, finish an upload, fetch a video's title and playlist address, change visibility. They never touch video bytes. This keeps them small and cheap, and a few dozen are plenty for 10,000 plays a second since most of those requests are served from cache.
Raw video storage. An object store (think of it as a giant, cheap, very durable hard drive reached over HTTP) holding the original uploads. The creator's app sends video parts straight here using short lived upload addresses, so 23 Gbps of ingest never passes through our API servers. We keep the original forever, because when a better codec arrives we want to re-encode from the source, not from an already compressed copy.
Task queue. When an upload finishes, an event lands on a queue. A queue is a waiting line of jobs. It decouples the upload from the slow processing, so a spike of uploads makes the line longer and not the system slower.
Transcode workers. Machines that turn the original into all the renditions. Transcoding means decoding the video and encoding it again at a different size and bitrate. This is the heaviest compute in the system, which is why we split it across thousands of cores (section 9.1).
Thumbnail step. Picks a few frames as candidate thumbnails and makes a small preview sheet, one tiny frame every 10 seconds, so the player can show pictures while the viewer drags the seek bar.
Segment storage. A second object store for the finished playlists and segments. It is separate from raw storage on purpose. Raw is private and written once. This one is read by the CDN and has different cache and lifecycle rules.
Metadata DB. Holds one row per video: title, owner, status, duration, where its playlist lives. Small and read heavy, so it gets a cache in front and read replicas behind.
CDN edge servers. A CDN (content delivery network) is a large group of cache servers spread over the world, close to viewers. The first viewer in a city asks for a segment, the edge server fetches it from us and keeps a copy. The next ten thousand viewers in that city get it from the edge. This is the box that carries almost the entire 3.1 Tbps.
Origin shield. One extra cache layer between the edges and our storage. Without it, a popular new video could be fetched from our storage once per edge location, say 200 times. With it, the edges ask the shield, and the shield asks storage once.
Event stream and count aggregator. Viewers send a small beacon ("I watched this video") and these flow through a stream, get added up in memory, and are written out in batches (section 9.3).
5.5 Why Most Traffic Never Touches Your Servers
Look at the viewer's path again. Step 5 is one small API call. Step 6 is hundreds of segment requests. Step 6 goes to the CDN, and with a 95 percent hit rate, 19 out of 20 of those requests are answered without our storage ever hearing about them.
Count the requests, not only bytes. A play of 6 minutes with 4 second segments is 90 segment requests. Out of those 90, about 4 or 5 reach our origin. The API call and a couple of beacons are the only things that always reach us. So our own servers see roughly 3 to 4 requests per play that matter, instead of 90. The CDN is not an optimisation bolted on afterwards. It is where the system lives.
6. API Design
Two groups: the creator side (upload) and the viewer side (play).
6.1 Creator Side
| Method | Path | Purpose |
|---|---|---|
| POST | /api/videos | Start an upload. Returns a video id and an upload session |
| GET | /api/uploads/:uploadId | Which parts does the server already have? Used to resume |
| PUT | <presigned url for part N> | Send one part directly to storage (not our API) |
| POST | /api/uploads/:uploadId/complete | All parts sent, please start processing |
| PATCH | /api/videos/:videoId | Change title, description, visibility, chosen thumbnail |
| DELETE | /api/videos/:videoId | Delete the video |
Start an upload:
{
"title": "Cooking pasta in 8 minutes",
"sizeBytes": 524288000,
"contentType": "video/mp4"
}Response 201 Created:
{
"videoId": "k3J9xQ2mLpA",
"uploadId": "up_81f2c",
"partSizeBytes": 8388608,
"totalParts": 63,
"expiresAt": "2026-10-11T10:00:00Z"
}The resume check returns the list of parts already received, for example parts 1 to 22, plus signed addresses for the next few parts. The server hands out signed addresses a few at a time. A signed address is a link that works for only one part and only for 15 minutes, so a leaked one is useless later.
6.2 Viewer Side
| Method | Path | Purpose |
|---|---|---|
| GET | /api/videos/:videoId | Title, owner, duration, view count, playlist address |
| GET | https://cdn.example/v/:videoId/master.m3u8 | The master playlist, served by the CDN |
| GET | https://cdn.example/v/:videoId/720p/seg_042.m4s | One segment, served by the CDN |
| GET | https://cdn.example/t/:videoId/thumb_1.jpg | A thumbnail |
| POST | /api/videos/:videoId/views | View beacon, sent after 30 seconds of watching |
The video info response holds the title, owner, duration, status, view count, and three CDN addresses: the playlist, the thumbnail and the preview sheet. Notice the segment and playlist addresses go to the CDN host, not to the API. Also note the video id is a random string, not a counter. Counters let anyone walk through every unlisted video.
For private videos the CDN URLs carry a signed token with an expiry time, so a link shared in a chat stops working after a few hours. For public videos no token is needed, and that keeps every cache entry identical for every viewer, which is what makes the hit rate high.
7. Data Model
Four kinds of data, with four different shapes: the video records, the upload and processing state, the segment files, and the counts. They do not belong in one store.
7.1 Videos (relational or key value, sharded by video id)
| Column | Type | Why it is there |
|---|---|---|
video_id | string, 11 chars | Primary key and shard key. Almost every read is "give me video X", so this spreads load evenly. Random, so ids cannot be guessed |
owner_id | bigint | Who owns it |
title, description | text | What the viewer reads |
status | enum | UPLOADING, PROCESSING, READY, FAILED, BLOCKED. The player only works when READY |
duration_seconds | int | Shown in lists, found by the probe step |
visibility | enum | PUBLIC, UNLISTED, PRIVATE |
original_key | string | Where the raw file lives |
playlist_key | string | Where the master playlist lives |
created_at | timestamp | Sorting |
A 1 KB row, about 900M rows after 5 years, so roughly 900 GB. The one lookup that does not fit the shard key is "all videos by this creator". For that we keep a second small table:
videos_by_owner
| Column | Why |
|---|---|
owner_id | Partition key. A channel page asks for one owner's videos |
created_at | Sort key, newest first |
video_id | The link back to the main table |
It is a copy, written along with the main row. A copy is cheaper than asking every shard "do you have any videos by this owner?".
7.2 Uploads
upload_sessions
| Column | Why |
|---|---|
upload_id | Primary key |
video_id | Which video this feeds |
part_size, total_parts | Fixed when the session starts, so both sides agree |
status | OPEN, COMPLETE, EXPIRED |
expires_at | Sessions die after 24 hours and the half sent parts are cleaned up |
Which parts have arrived is not stored by us. The object store already tracks the parts of a multipart upload, so we ask it. One less thing to keep in sync.
7.3 Renditions and Transcode Tasks
renditions
| Column | Why |
|---|---|
video_id | Part of the key |
name | 240p, 720p and so on. Together with video_id this is the primary key |
bitrate_kbps, width, height, codec | What the master playlist advertises |
playlist_key | Where that rendition's playlist lives |
size_bytes | Used for cost reports and cleanup |
status | PENDING, READY, FAILED. Lets us publish the small renditions first |
transcode_tasks
| Column | Why |
|---|---|
task_id | Primary key |
video_id | Which video this belongs to |
kind | PROBE, SPLIT, ENCODE_PIECE, AUDIO, PACKAGE, THUMBNAILS |
piece_index, rendition | Which 12 second piece and which size, for ENCODE_PIECE |
status | QUEUED, RUNNING, DONE, FAILED |
attempts | After 3 failures we stop and mark the video failed |
lease_until | A worker holds a task for a limited time. If it dies, the lease runs out and another worker takes over |
The task table is the memory of the pipeline. If the whole scheduler restarts, it reads this table and carries on.
7.4 The Files Themselves (object storage)
Not a table, but the key layout matters:
raw/{videoId}/original.mp4
v/{videoId}/master.m3u8
v/{videoId}/{rendition}/index.m3u8
v/{videoId}/{rendition}/seg_{NNN}.m4s
t/{videoId}/thumb_{1..3}.jpg
t/{videoId}/sheet.jpgEverything under a video's prefix can be deleted in one go. Segment files are immutable: once seg_042.m4s exists it never changes, so the CDN can cache it for a year.
7.5 View Counts
video_view_counts (key value store)
| Column | Why |
|---|---|
video_id | Key |
total_views | The number shown under the video |
updated_at | When the last batch landed |
view_events is an append only log in the event stream, then a column store for reports. Each event has video_id (the stream partition key, so all events for one video meet at one aggregator), session_id (to ignore repeated beacons), watched_seconds, country, device and event_time.
Two stores for counts on purpose. The key value row is only for the fast, approximate number on the page. The event log is the truth, used later for creator payouts and dashboards.
8. Key Flows
8.1 Upload With Resume
The creator's phone only re-sends what is missing. On a 500 MB file that dropped at part 22 of 63, resuming saves 22 x 8 = 176 MB of re-upload. The part size matters: 8 MB is large enough that we do not make thousands of requests, small enough that a lost part costs seconds, not minutes.
8.2 Processing Pipeline
8.3 Playing a Video
Notice that the player starts at a low rendition. A 360p first segment is about 400 KB (0.8 Mbps x 4 s = 3.2 Mbit). It arrives quickly, the picture appears fast, and the player climbs to better quality once it has measured the connection. Starting at 1080p would be a 2.5 MB first download and a longer spinner.
9. Deep Dives
9.1 Transcoding: One Big Job or Many Small Ones
The problem. Turning an 8 minute video into 5 renditions is about 40 core minutes of work. If one machine does it start to finish, the creator waits 40 minutes in the best case, and a one hour video waits five hours. If the machine dies at minute 38, you start over.
Option A: one worker per video. Simple. A worker pulls a video, runs the full ladder, writes the outputs. Easy to reason about, and fine for a startup with 50 uploads a day. But the time to publish grows with video length, and failures are expensive.
Option B: split first, then encode pieces in parallel. A first step cuts the original into pieces of 12 seconds at points where the video can be cleanly cut (the encoder calls these keyframes). Twelve is a multiple of our 4 second segment length, so piece edges line up with segment edges. Then each piece x each rendition becomes its own small task.
8 minutes = 480 s / 12 s = 40 pieces
40 pieces x 5 renditions = 200 encode tasks
Each task ~ 12 s of video, takes maybe 10 to 30 seconds of CPUWith free workers, the whole ladder finishes in a minute or two instead of 40 minutes. A one hour video makes 300 pieces and 1,500 tasks, and finishes in about the same time, because the work is spread wider, not longer.
The choice: B. Three reasons.
First, speed. The creator sees their video within minutes, whatever its length.
Second, cheap failure. If a worker dies, only a 12 second task is lost, not the whole video. The lease_until column handles this: a worker that stops reporting loses its task to someone else.
Third, cheap machines. Tasks are small and safe to repeat, so you can run workers on spot instances (cheap machines the cloud may take back at any moment). That can cut compute cost by half or more.
Two details people forget. Tasks must be safe to repeat: every task writes to a fixed, predictable file name, so running it twice just overwrites the same file with the same bytes. And publish early: finish the 360p rendition first and mark the video playable with only the small renditions, then add the larger ones as they complete. Viewers get the video sooner, and the player simply sees a shorter ladder until the rest arrives.
The cost of B is a scheduler and a task table, so there is more machinery to run. For a hobby project, A is fine. At 500,000 uploads a day, B pays for itself.
9.2 CDN: Pushing Everything or Pulling on Demand
The problem. 300M new segment files appear every day. Only some will ever be watched, and even those are mostly watched in a few cities. How do they get to the edge servers?
Option A: push. After transcoding, copy every segment to every edge location ahead of time. Viewers always find the file at the edge, even the first viewer. But look at the numbers: 550 TB of new data a day copied to maybe 200 locations is 110 PB of copying a day, to cache files that mostly nobody asks for. Most videos get under 100 views in their life. This wastes more bandwidth than it saves.
Option B: pull on demand. The edge starts empty. On the first request it fetches from the origin shield, keeps the copy, and serves everyone after that. The first viewer in each city pays a slightly slower start, and every later viewer is fast. Unpopular videos cost almost nothing, because they are only copied when someone wants them.
The choice: B, with two additions.
An origin shield, so that a miss at 200 edges turns into one fetch from storage and not 200. Without it a viral upload can hit your storage with 200 identical requests for every segment in the first seconds.
Request collapsing at each cache. If 5,000 viewers all ask for the same uncached segment in the same second, the cache sends one request upstream and holds the others for the few milliseconds it takes. Without it, a miss on a hot segment turns into 5,000 requests to the origin, all at once, the classic thundering herd.
Then a small piece of push, only where we know it pays. If a creator with 20 million subscribers publishes a video, we know a huge crowd arrives in minutes. For those, after processing, we send a "warm up" request through the CDN for the first 30 seconds of each rendition, so the first wave of viewers does not all miss together. Everyone else is pulled on demand.
Caching rules that make this work:
| Item | Cache time | Why |
|---|---|---|
Segments (.m4s) | 1 year, marked immutable | Never changes once written |
| Rendition playlists | 1 year once the video is READY | VOD playlists do not change |
| Master playlist | 1 hour | Might gain a new rendition (like AV1) later |
| Thumbnails | 1 day | Creator can change the thumbnail |
| Video info API | 30 seconds, plus cache clear on edit | Title and counts can change |
Also, a point on the long tail: very old, rarely watched videos fall out of the edge caches (least recently used goes first). The shield keeps them a bit longer, and after that they come from storage, slowly and cheaply. A cold tier for videos nobody has watched for a year saves real money: perhaps a quarter of the normal storage price, at the cost of a slow first byte.
9.3 View Counts at Scale
The problem. Every play adds one to a number. On average that is 3,500 increments a second, which any database can take if the writes are spread over many rows. But one viral video might get 200,000 plays a second, all hitting the same row. A single database row can handle perhaps a thousand updates a second before the writes queue up behind each other waiting for the row lock.
Option A: increment the database row on every play. Simple and exact. It works for ordinary videos and falls over for exactly the videos you care most about, at the worst time.
Option B: collect events, add them up in memory, write in batches.
The beacon is fired by the player after 30 seconds of real watching, not on page load. That one rule filters out accidental clicks and a lot of bots, and it cuts event volume. The events go onto a stream, partitioned by video id, so every event for one video arrives at the same aggregator. The aggregator keeps a small map in memory: video_id -> count since last flush. Every 5 seconds it writes the totals.
The viral video now produces 200,000 events a second into the aggregator, which is easy to absorb in memory, and one database write every 5 seconds. A thousand times fewer writes on the hot row.
The choice: B. Cost: the number on the page is up to 5 to 10 seconds behind. Nobody can tell. If an aggregator crashes it loses at most 5 seconds of in memory counts, but the events are still in the stream, so on restart it reads from its last saved position and recounts.
The number under the video is a good estimate. The number used to pay creators comes from the nightly job over raw events, which can also remove repeats from the same session, check for fake traffic and be exact. Say this in an interview: two numbers, one fast and approximate, one slow and exact, and they are allowed to differ by a small amount.
A Redis counter would also be fine for the live number, with the same batching idea feeding it. What I would avoid is making the database row the thing every player talks to.
10. Failure Handling
| What breaks | What the user sees | What we do |
|---|---|---|
| Creator's connection drops mid upload | Progress bar pauses, then continues | Resume from the last good part using the session id |
| Transcode worker dies | Video takes a little longer to appear | Lease expires, another worker redoes just that task |
| One piece keeps failing (a corrupt part of the file) | Video shows FAILED with a reason | After 3 attempts stop, keep the original, tell the creator |
| One CDN edge server dies | Maybe a tiny hiccup | Requests go to another edge in the same location. The player retries failed segments |
| Whole CDN region has trouble | Slightly slower starts | DNS steers viewers to the next nearest region. A second CDN provider is the backup for big events |
| Origin storage slow or down | Popular videos keep playing. Rarely watched or brand new ones fail | The CDN keeps serving cached segments even past their expiry while the origin is unhealthy |
| Metadata DB primary down | Video pages still load from cache. Edits fail briefly | Promote a replica. Reads come from cache and replicas anyway |
| A segment is missing or corrupt | One short stutter | The player retries, then switches to another rendition for that part |
| Event stream down | View counts pause | Beacons are small and lossy by design. A few lost counts are fine, and playback is unaffected |
| Viral spike on one video | Nothing, ideally | Request collapsing, origin shield, warm up for known big releases |
| Copyright takedown | Video disappears | Set status BLOCKED, then purge its prefix from the CDN so it stops being served |
The pattern is the same as in other designs where one side matters more. Playing existing videos must keep working when almost anything else is broken. Uploading and counting can fail loudly.
11. Tradeoffs
| Decision | Chosen | Alternative | Reason |
|---|---|---|---|
| Delivery format | HLS with 4 second segments | One big file, or a custom streaming server | Plain files can be cached by any CDN, and seeking becomes a file lookup |
| Quality switching | Player decides (ABR) | Server decides | The device knows its own network best. Servers stay stateless |
| Transcoding | Split into pieces, run in parallel | One worker per video | Minutes instead of hours, and cheap retries. Costs a scheduler |
| Codec | H.264 for all, AV1 only for popular videos | AV1 for all | Encoding cost is paid only where the bandwidth savings repay it |
| Where bytes flow | Direct to object storage with signed addresses | Through our API servers | Keeps 23 Gbps off our servers |
| CDN fill | Pull on demand, push only for known big releases | Push all | 300M files a day, most never watched |
| View counts | Batched, a few seconds stale | Exact increment per play | Protects the hot row on viral videos |
12. Interview Talking Points
- Start with the questions: on demand or live, how many plays and uploads, how long are videos. Then say you are only designing on demand, because live changes the whole pipeline.
- Do the egress arithmetic first. 300M plays x 112 MB is 33.6 PB a day, about 3 Tbps. Say plainly that no origin can serve this, and that is why the design is built around a CDN.
- Explain segments in one breath: cut each size of the video into 4 second files, list them in a playlist, and the player downloads them like pages of a book. Then say the player picks quality per segment, so servers stay simple.
- Walk the upload path: resumable parts sent straight to object storage with signed addresses, so API servers never carry video bytes. Mention 8 MB parts and what resume saves.
- Explain why transcoding is split into 12 second pieces and run in parallel, with a task table, leases and retries. Add the two small touches: publish the low renditions early, and use cheap interruptible machines.
- Give the cost view without being asked: egress is the bill, not storage. Pull based CDN with an origin shield, immutable segments cached for a year, AV1 only for popular videos, cold storage for the long tail.
- On view counts, name the hot row problem, then give the batched stream design with a 30 second rule, and say there are two numbers, one fast and one exact.
- Finish with failure: viewers keep watching while uploads, counts and even the database are having a bad day. Playing existing videos is the thing you protect.