Library
Learn/System design/Chapter 32
33 min read

Design YouTube

In December 2014 the view counter under Gangnam Style ran out of room. The video had passed 2,147,483,647 views, the largest number a signed 32 bit integer can hold, and YouTube had to move the counter to 64 bits. Nobody had planned for one video to be watched more than two billion times. That is the kind of problem this chapter is about.

You already know how to store and stream the video file itself. That is the video streaming chapter: upload in parts, transcode in chunks, serve segments from a CDN. This chapter is everything around the player. The number under the video, the subscriptions tab, the bell, the comments, and the copyright check that runs before anyone sees an upload. In an interview, "Design YouTube" is mostly these parts, and they are where the hard problems sit.

Step 1: Understand the problem#

"Design YouTube" is huge, so the first job is to agree on which part you are building. Ask these in the first three minutes.

You askThey sayWhat it settles
Is the video pipeline part of this, and the home page recommendations?No. Assume upload, transcoding and playback exist. Treat the home page ranking as a service another team owns.You can spend the round on the platform around the player. Say in one sentence how the pipe works and point at it, so the interviewer knows you are cutting it on purpose.
How big?Assume a billion watch page loads a day. Some channels have hundreds of millions of subscribers.The watch page is a read problem a cache solves. The big channels are what break a simple subscriptions feed, so they get their own deep dive.
How exact does the view count have to be?Close enough, within a few minutes, for viewers. Exact, with fake views removed, for paying creators.Two numbers from two pipelines: a fast one for display and a slow, careful one for money. This single answer shapes the whole counting design.
Who gets told when a channel uploads?Every subscriber sees it in their subscriptions tab. Only those who turned the bell on get a push notification, within minutes.Two different audiences of two different sizes. The feed can be slightly late, the push can be spread over a few minutes, and neither has to be instant.
How are comments ordered?Top comments by default, newest first as an option.Top order cannot be a sort at read time on a video with a million comments. It becomes a list you compute ahead of time.
Do we check uploads for copyright?Yes. Match each upload against reference files that rights holders have supplied, before it goes public.A gate in the publish path that can block a video or hand its money to someone else. How the matching works is its own system, so you design the gate, not the matcher.

What you are building, and what you cut#

In scope
  • The watch page: title, channel, counts, and your own state. One call that must be fast for a billion loads a day.
  • View and like counts. Approximately right in minutes, exactly right by the next day.
  • Subscriptions tab and bell notifications. A news feed with very uneven channel sizes.
  • Comments, with top and newest ordering. Write light, read heavy, and very skewed.
  • The Content ID gate before publish. A step in the publish path, with the matcher treated as a black box.
The numbers you commit to
  • Watch page metadata in under 200ms at p95.
  • Displayed counts no more than a few minutes behind.
  • Bell notifications delivered within about 10 minutes of publish.
  • Counts used for creator payouts are exact and published daily.
Cut, and say so out loud
  • Upload, transcoding and the CDN. Covered in the video streaming chapter.
  • Recommendations and the home page. A ranking system, and it belongs with the AI path.
  • Search. The same shape as the web search engine chapter, over titles and captions.
  • Live streaming and ads. Each one is its own design.

Back of the envelope#

Move the sliders. Watch which number grows fastest.

Where the load is
1 billion
6
3
Watch pages per second1 billion / 86,400 = 11,574 /s
At the evening peakaverage x 3, an assumption = 34,722 /s
Heartbeats per secondone every 30s for 6 min = 138,889 /s
Likes per second3 per 100 views = 347 /s
138,889 /s
heartbeat events, and every one is a write

The watch page is 11,574 reads a second, and nearly all of them are the same few thousand popular videos, so a cache absorbs it. The heartbeats are 400 times the likes and every one is a write. That stream must never touch a database row directly, which is what the first deep dive is about.

The number people get wrong

Candidates average the views across all videos and conclude a counter column is fine. The average hides the shape. Ten million views of one trailer in its first hour is about 2,800 a second, all on one row. A single row in a relational database manages a few thousand updates a second at best before the lock queue becomes the bottleneck, and that is one video on one good day.

Step 2: Propose the high level design#

The API#

GET/v1/videos/{id}
returns 200 { title, channel, publishedAt, views, likes, manifestUrl }
Why: Identical for every viewer, so it can be cached for a minute anywhere, including at the CDN. The counts in it are the approximate display numbers, and saying so out loud is part of the answer.
GET/v1/videos/{id}/me
returns 200 { liked, subscribed, bell, resumeAtMs }
Why: The few personal fields, in their own tiny call. Put "did you like this" into the main response and every response becomes unique to one viewer, which makes the busiest endpoint you own uncacheable.
PUT/v1/videos/{id}/rating
{ "value": "like" }
returns 204
Why: PUT sets a state rather than adding one. A double tap, a retry on a bad network, or two open tabs all leave the same single row, so they cannot inflate the count.
PUT/v1/channels/{id}/subscription
{ "notify": "all" }
returns 204
Why: The bell is a field on the subscription, not a separate feature. The notification fan out reads only the subscribers whose field says "all", which is a much smaller set than everyone.
GET/v1/videos/{id}/comments?order=top&cursor=...
returns 200 { comments: [...], nextCursor }
Why: A cursor, not a page number. New comments arrive while you scroll, and an offset would show you the same comment twice. Views come from the heartbeat endpoint in the video chapter, so there is no "add a view" call here at all.

The data model#

Three tables carry the platform. The first one shows the most important idea in this chapter: the row is the truth, and the count is something you work out from the rows.

ratingsMySQL, sharded by video_id
video_idbigintPKShard key, so every rating for one video lives together.
user_idbigintPKOne row per viewer per video. This is what makes a double like impossible.
valueenumlike, dislike or none. Unliking sets none rather than deleting, so the change is visible to the counting pipeline.
updated_attimestamp
Sample row
88123 | 5521 | like | 2026-10-19 18:02:11
There is no like_count column anywhere. When a rating actually changes (none to like, like to none), the same transaction writes a +1 or -1 event to the counting pipeline. A repeated PUT that changes nothing writes no event, which is how the count stays honest.
subscriptionsMySQL, stored twice
subscriber_idbigintPKThe copy sharded by subscriber answers "which channels do I follow".
channel_idbigintPKThe copy sharded by channel answers "who follows this channel", which the fan out reads.
notifyenumIDXall, personalized or none, the three bell settings YouTube offers. Indexed with channel_id so the bell set is one range scan.
created_attimestamp
Two copies, because the two questions shard in opposite directions. A single table sharded by subscriber would make every upload a query to every shard. Keep them in step by writing the second copy from an event, and accept a second or two of lag.
commentssharded by video_id
video_idbigintPKShard key. Every comment on a video is on one shard, so loading a page of them is one query.
comment_idbigintPKTime ordered, from the unique ID generator, so "newest first" is a plain range read.
parent_idbigintIDXNull for a top level comment. Replies go one level deep, which keeps a thread to two reads.
author_idbigint
bodytext
stateenumvisible, held for review, removed. Hiding a comment is a state change, never a delete.
Like counts on comments come from the same counting pipeline as video likes. The top comments order is not a column either. It is a list computed in the background, covered in the third deep dive.
What YouTube actually ran

YouTube kept its metadata in MySQL, and around 2010 a single database could no longer hold it. The team built Vitess, a layer that splits MySQL across many shards and sends each query to the right one, then open sourced it. It is now a CNCF graduated project used far outside Google. So "sharded MySQL" here is not a compromise, it is the real history.

The whole system on one whiteboard#

Figure 1 has three paths. The top row runs once per upload. The middle row runs a billion times a day. The bottom row is where the counts come from.

Figure 1. Nothing on the watch path writes to a database. Counts arrive in the cache from the counting pipeline, and subscription feeds are filled in ahead of time by the publish path along the top.

Walking Figure 1:

  1. The upload pipeline from the video chapter finishes transcoding and hands the video on. It is still private.
  2. Content ID compares the video's audio and picture fingerprints against the reference files rights holders have supplied. A match applies the owner's chosen policy: block the video, run ads and pay the owner, or just track it. Only then is the video published.
  3. Fan out workers write the new video's id into the subscription feed of every subscriber, for channels below a size threshold. Big channels skip this step (the second deep dive).
  4. Subscribers with the bell on get a push notification, sent out over a few minutes rather than all at once.
  5. A viewer opens a video. The watch API makes two calls in parallel: the shared part, which is the same for everyone, and the small personal part.
  6. The shared part comes from a Redis cache. For a popular video it is always there.
  7. On a miss, the cache loads the video's row from the metadata shard that owns it.
  8. Comments are loaded the same way, but only when the viewer scrolls down to them.
  9. The subscriptions tab reads the viewer's own feed list, filled in ahead of time by step 3.

The two dashed lines are the counting path. The player sends a heartbeat every 30 seconds, heartbeats and like changes go into the counting pipeline, and about once a minute the pipeline writes fresh counts into the cache. Writes like a new comment or a rating go through the watch API to their own tables, and are left off the figure so the read path stays readable.

Your answer

Figure 1 never adds one to a view count in a database. Where does the number under the video actually come from, and what would go wrong if it were a column you updated on every view?

Step 3: Design deep dive#

Counting a billion views without a hot row#

Start with the obvious design, because the interviewer wants to hear you reject it. Every view runs UPDATE videos SET views = views + 1. Each update locks the row, so updates to one video queue up behind each other. On a normal video that is fine. On a trailer at 2,800 views a second, the queue grows faster than it drains.

Compare three places the +1 can go.

Where the +1 goes
Simple, exact on paper, and it falls over on the first viral video, because every view of that video waits for the same lock. It also counts whatever arrives, including a bot reloading the page in a loop. Nothing here can tell a real view from a fake one.

The log design gives you two numbers, and you should say so before you are asked. The display count is fast and roughly right: the stream job drops obvious repeats, such as the same session counted twice in a minute, and moves on. The payout count is slow and exact: the daily job has the whole day of events, so it can remove a session that watched 400 videos in an hour, or ten thousand views from one block of addresses. When the daily number is ready it replaces the display number. It is the same split as the ad click aggregator, for the same reason: money needs a careful count, and a screen needs a quick one.

What counts as a view? YouTube does not publish its rule, and you should not pretend to know it. A defensible answer: a view is a session whose heartbeats show at least 30 seconds of playback, or the whole video if it is shorter. Then say it is a product decision you would make configurable, because it will change.

The 301 views

For years, new videos on YouTube appeared to freeze at around 301 views while the real number climbed. The public count was held while YouTube checked that early views came from people, then released once the check finished. It is the two numbers idea, visible on the page, and in 2015 YouTube changed the system so counts updated more often.

There is one more trap inside the log design. Kafka spreads events over partitions using a key, and the natural key is the video id, because then one stream worker sees every event for a video and can add them up. Watch what happens to that key when one video gets most of the traffic.

Partition key
13
12
13
12
13
12
13
12
0
1
2
3
4
5
6
7
Partition 2 carries 13.3% (even would be 12.5%)
Millions of videos hash across eight partitions and the load comes out even. Every worker does about the same work. This is why video_id looks like a perfect key in testing.

Salt every key, or only the hot ones? Salting every video costs the extra adding step on millions of videos that get a few views a day. A common answer is to salt only videos the pipeline has seen go past a rate threshold in the last few minutes. I have not run this at YouTube's scale, so treat the exact threshold as something you would measure.

Subscriptions: one upload, 250 million subscribers#

The subscriptions tab is a news feed, and the news feed chapter already gives the answer: write the new post into each follower's list (push), except for accounts so big that the writes would take too long, which readers pull in when they open their feed. YouTube has more of those big accounts than most networks. India is YouTube's largest audience, and a few channels, T-Series among them, have passed 250 million subscribers. A channel like that cannot fan out with one write per subscriber.

YouTube adds one twist the plain news feed does not have: the bell. Here is one upload from a channel that size, step by step.

1/6 The publish event carries the channel id. The fan out service looks up the subscriber count first, because the count decides which path the upload takes.
Figure 2. The subscriptions tab and the bell are two separate audiences. The tab can be filled in lazily. The bell is a smaller set, and its real danger is not sending pushes but what happens when everyone taps them at once.
The follow up you will get

"How do you pick the threshold?" Do not guess a number and defend it. Say what it trades: below it you pay write work at upload time, above it you pay merge work on every read of the subscriptions tab. Start around 100,000 subscribers, then measure fan out lag and tab read latency and move the line until neither is the problem. Being able to move it without a deploy is the real requirement.

Comments: write light, read heavy#

A million comments on a video sounds like a write problem. It is not. A million comments in a day is about 12 a second, which one shard handles without noticing. The problem is the read: everyone who opens the video and scrolls down asks for the same first page of top comments, and "top" means sorted by a score that keeps changing.

Top comments
Always fresh, and it reads a million rows to return twenty, for every viewer, on the one shard that holds this video. On a busy video that shard spends its whole day sorting the same comments.

Your own new comment is the one exception. If you post a comment and do not see it, you post it again. So the API puts the viewer's own recent comments at the top of their view, from the write it just made, even though the ranked list does not contain them yet. This is the same "read your own writes" fix the consistency article describes, done in the API instead of the database.

Break it#

A big film trailer goes live at noon on a channel with a lot of bell subscribers. Drag the dial and watch which box goes first.

Trailer launch
normal noon
normal noonpushes in one burstcache entry expirescounter frozenrepaired
Healthy. The cache answers nearly every watch page. The metadata shards see a trickle of misses from the long tail of old videos. Counts lag by about a minute.
Read every step as text
  1. normal noon · Healthy. The cache answers nearly every watch page. The metadata shards see a trickle of misses from the long tail of old videos. Counts lag by about a minute.
  2. pushes in one burst · Degraded. All bell notifications go out in the same minute. Millions of taps arrive before the watch API can add servers, so request latency rises. The cache is fine, because it was filled at publish. Spreading the pushes over ten minutes removes this spike.
  3. cache entry expires · Down. The trailer’s cache entry hits its expiry time at the peak. Every API server misses at once and sends the same query to the one shard that owns the video, thousands of times a second. That is a cache stampede. The fix is to let one request per key refill the cache while the others wait, and to refresh hot keys before they expire.
  4. counter frozen · Degraded. Heartbeats are keyed by video id, so the trailer’s whole stream lands on one partition. That partition falls twenty minutes behind and the view count stops moving on the most watched video of the day, which is exactly where people look. Salting the hot key spreads it out.
  5. repaired · Healthy. Pushes spread over ten minutes, one refill per key with early refresh for hot entries, and salted keys for hot videos. None of the fixes adds a new box. Each one changes when or where work happens.

The cache stampede is the one to raise without being asked. It only happens on your most popular video at its most popular moment, so it is the outage everyone sees.

Trade-offs#

ChoiceWhat you gainWhat you payPick it when
Count from a log, not a rowNo lock on a hot row, one write per video per minute, and raw events kept for a careful recount.The displayed number is a minute or two behind, and you run a stream job and a batch job instead of one UPDATE.Any counter that a single item can push past a few thousand updates a second, or that money depends on.
Two numbers: display and payoutViewers get a fast number and creators get an exact, fraud filtered one, each from the pipeline suited to it.Two numbers can disagree for a day, and you must explain why a count went down when the recount removed fake views.Whenever a count is shown in public and also paid out on.
Push for small channels, pull for big onesUpload cost stays bounded for the biggest channels, and the subscriptions tab read stays one list plus a short merge.Two code paths, and a threshold you have to tune and be able to change.Any follow graph where a few accounts have millions of times more followers than the average.
Ranked comment list computed aheadReading top comments is a cache slice, not a sort over a million rows.The order is up to a minute old, and you need a special case so people see their own new comment.Any "top" ordering on a list that is read far more often than it changes.
Shared and personal watch page callsThe shared part caches for everyone, including at the CDN.Two requests per page instead of one, and the client has to stitch them together.Any page where almost everything is the same for every viewer and a few fields are not.

Interview replay#

Interviewer
Design YouTube.
The whole prompt. They are waiting to see what you cut.
You
The video pipe is a known shape: upload in parts to object storage, transcode in chunks, serve segments from a CDN. I would describe it in a minute and then spend the round on the platform around it, because that is where the interesting load is: the view counter, the subscriptions feed for very large channels, and comments. I would treat recommendations as a service I call. Does that split work for you?
Scopes out loud and asks for agreement. The interviewer can redirect now instead of at minute 30.
Interviewer
Fine. How do you count views?
The question they came for.
You
Not with an UPDATE. A trailer can get thousands of views a second on one video, and one row cannot take that. The player sends a heartbeat every 30 seconds into Kafka. A stream job adds them up per video in one minute windows and writes the total into the cache, so it is one write a minute per video. The raw events are kept, and a daily job recounts them with deduplication and fraud filtering. That second number is the one creators are paid on, and it replaces the display number when it lands.
Rejects the naive design with a number, then gives both pipelines and says which one money uses.
Interviewer
What exactly counts as a view?
A test of whether you will make something up.
You
I do not know YouTube’s actual rule, they do not publish it. I would start with a session that plays at least 30 seconds, or the whole video if it is shorter, and make the threshold a setting. It is a product decision, and the heartbeat data lets us change it later and recount.
Says "I do not know" plainly, then gives a usable default and the reason it can change.
Interviewer
A huge channel uploads. What happens?
Checking for the celebrity problem.
You
Above a threshold we skip writing into every subscriber’s feed. The video goes into the channel’s own recent uploads list, and when a viewer opens their subscriptions tab we merge their feed with the recent uploads of the few big channels they follow. Bell subscribers are a separate, smaller set. We push to them, but over ten minutes, because if everyone taps in the same minute the watch page takes the hit. And we fill the cache before the first push.
Reuses the news feed answer, then adds the bell and the stampede on the watch page, which is the YouTube specific part.
Interviewer
The view count on today’s biggest video stopped moving. Everything else is fine. Why?
A debugging question. One video broken, everything else fine, is a strong clue.
You
One video broken while others are fine points at something keyed by that video. Most likely its heartbeats all hash to one Kafka partition, and that partition is behind. I would check consumer lag per partition. If one partition is far behind and the rest are not, that is it, and the fix is to salt the key for hot videos so they spread across several partitions and add the parts up afterwards.
Reasons from the symptom to a cause, names how to confirm it, then gives the fix.

Checkpoint#

Checkpoint

1. Why did Gangnam Style force YouTube to change its view counter in 2014?

2. Heartbeats are keyed by video_id in Kafka. One trailer gets a huge share of traffic. What is the fix?

3. Same design, but likes now decide a live vote and must be exact in real time. What changes first?

Worth memorising
  • 2,147,483,647. Gangnam Style broke the 32 bit counter in December 2014. Use 64 bit counters from day one.
  • One row takes a few thousand updates a second at best. One trailer can pass that alone, so counts come from a log, never from +1 on a row.
  • Two numbers. Display: approximate, minutes behind. Payout: exact, fraud filtered, daily.
  • Push below the threshold, pull above it, and spread bell pushes over about ten minutes. The stampede is on the watch page, not the notification service.
Say this in 60 seconds

I would cut the video pipe early, since upload, chunked transcoding and CDN delivery are a known design, and spend the time on the platform around the player. The watch page is split into a shared part that caches for everyone and a tiny personal part, so a billion loads a day mostly hit a Redis cache in front of sharded MySQL. View counts never touch a database row: the player sends heartbeats into Kafka, a stream job adds them up per video every minute and writes the total to the cache, and a daily job recounts the raw events with fraud filtering for creator payouts. Hot videos get a salted key so one trailer does not swamp one partition. Likes are one row per viewer, set with a PUT, and a change emits a plus or minus one event into the same pipeline. Subscriptions are a news feed: push into feeds for small channels, pull at read time for channels with hundreds of millions of subscribers, and send bell notifications over ten minutes so the watch page does not get a stampede. Top comments are a list ranked in the background, not a sort at read time. Content ID sits in the publish path as a gate. The failure I would call out is the trailer's cache entry expiring at its peak, so hot keys refresh early and only one request per key refills the cache.