Skip to content

AI systemsProblems

Design a video recommendation feed

Design a TikTok-style video recommendation feed, step by step. Grow one architecture from a trending list to two-tower candidate retrieval, a multi-task ranker with a feature store, real-time session signals and exploration for new videos, with estimates, evals and code in Java, Python, C#, Go and TypeScript.

Senior34 min core · 52 min full depth

“Design the For You feed of a short video app. A user opens the app and swipes. Every next video should be one they want to watch.”

This is the classic machine learning design question. The model is the easy part to describe. The hard parts are around it:

  • Choosing 10 videos out of a billion, in a fifth of a second.
  • Reacting to a swipe within the same session.
  • Giving 10 million new uploads a day a fair chance, when nobody has watched them yet.

We start with a plain trending list, and add one part at a time, only when a number forces us to. LLMs aren’t the core of this system, and the page shows why.

If you only have a minute, this is the design. The rest of the page explains each part.

  1. A funnel. No model can score a billion videos per request. So a cheap step finds about 3,000 likely videos, a small model keeps 500, and a big model orders those. We show 10.
  2. The big model predicts several things: will they finish it, like it, share it, or tap “not interested”. A weighted sum, chosen by the product team, turns them into one score.
  3. Fresh inputs, not a fresh model. A stream turns each swipe into session signals within a second. The next page uses them.
  4. 5% of slots test new videos, so new uploads get an audience and the feed doesn’t narrow.
  5. Every step has a cheaper fallback. The last one is the trending list, which never fails.

A few questions change the design a lot. For the rest, we assume an answer and move on.

Interviewerminute 0

Design the For You feed of a short video app.

What they are testing

Whether you ask what “good” means before anything else. The goal shapes the model, the labels and the metrics. Asking about video encoding or comments spends time on a different system.

These five questions matter most:

  • What do we optimize? This one matters most. We aim for long-term satisfaction: watch time and finishing videos. “Not interested” and reports act as guardrails, limits a change must not break. Optimizing clicks alone builds a clickbait machine within weeks.
  • How many users? 200 million a day, about 100 videos each.
  • How big is the catalog? 1 billion videos, plus 10 million uploads a day.
  • How fast must the feed react? A skip changes the next page. A new upload can reach viewers within an hour.
  • Discovery or follows? Discovery: anything in the catalog. Followed creators are one source.

We leave out upload and video delivery (see video streaming), moderation, ads and search. We receive a “video ready” event once a video is processed and checked.

Go deeperThe full requirements and quality targets3 min

Must do. Return pages of 10 videos, endlessly, with no video the user saw in the last 30 days. Views, skips, likes, shares, follows and “not interested” change later pages, including the very next one. A new upload that passes checks can reach viewers within an hour. A new user gets a reasonable feed on the first open. Removed videos are never shown, and age and country rules hold.

Speed. Under 200 ms of server time per page for 99% of requests. The app fetches the next page early, so users never feel it.

Availability. 99.95%. The feed always returns something. A worse page beats an error.

Freshness. A skip reaches the next page within 2 seconds. Video counters lag under a minute. Models are under 36 hours old.

Quality.

  • Retrieval finds tomorrow’s finished videos as often as production does, within 1%.
  • Ranking is no worse than production on next-day logs, and each prediction is accurate in every score range, within 5%.
  • Online, watch time per user wins its A/B test, and no guardrail gets worse by more than 1%.
  • New videos: 95% of eligible uploads get 200 views within 24 hours.
  • Variety: at most one video per creator on a page.
More questions We assume If the answer were different
Regions and languages? Global. Most users watch in their own language One market fits one region, and trending is simpler
Ads in the feed? An ad server fills reserved slots Blending ads turns the last step into an auction
A small catalog? No, 1 billion 20,000 titles could be scored in full, with no retrieval step
Interviewerminute 3

Do you need machine learning for this? Couldn’t you just show what’s popular?

What they are testing

Whether you start from the cheapest thing that works and say exactly where it falls short. Jumping to a deep network without mentioning popularity reads as memorized. So does putting an LLM in the main path without doing the math.

Start from the cheapest option, and move up only where it falls short:

Why each cheaper option falls short

everyone sees the same feed

new videos have no co-watches

42 billion tokens a second

Popular in your region

Watched this, also watched that

A learned funnel: find candidates, then rank them

Picked. It works for new users and new videos too

An LLM ranks every candidate

Ruled out

everyone sees the same feed

new videos have no co-watches

42 billion tokens a second

Popular in your region

Watched this, also watched that

A learned funnel: find candidates, then rank them

Picked. It works for new users and new videos too

An LLM ranks every candidate

Ruled out

Scroll or pinch to zoom, drag to pan
  • Color key
  • Too simple
  • The funnel
  • Picked
  • Too costly
  • Ruled out

Popular videos work as a first version and as the fallback. But everyone sees the same feed. “Watched this, also watched that” is cheap and strong, but a new video has no co-watches. So we pick a learned funnel: a step that finds a few thousand likely videos, then models that rank them. The models read features, like topic, language and creator, not just IDs. That is how they handle new users and new videos.

Why not an LLM? About 3,000 candidates at roughly 200 tokens each is 600,000 tokens per page. At 70,000 pages a second, that is 42 billion tokens a second. LLMs help offline instead: tags, captions and text summaries of videos that become features.

Interviewerminute 6

How big is this? What does one feed request cost?

What they are testing

Whether the numbers size the funnel: requests per second, candidates and scores per request, GPUs, feature reads and events, and what each one decides. Storage for video files belongs to a different system.

Assumptions
Daily active users
200 million
Videos viewed per user per day
100
Videos per feed page
10
Peak over average
3×
Candidates per request
3,000
Videos the heavy ranker scores
500
Heavy ranker scores per second per GPU
100,000
Videos in the retrieval index
1 billion
Bytes per indexed video (compressed vector + id)
72 bytes
Bytes per event
200 bytes
Days of raw events kept
90
Feed requests23,000/sabout 70,000/s at peak

200 million users × 100 videos ÷ 10 per page is 2 billion pages a day. Divide by 86,400 seconds. Evenings run about three times the average.

So we size every step for 70,000 pages a second, and keep the feed servers stateless.

Big model35M scores/sabout 600 GPUs at peak

70,000 requests × 500 videos. One GPU scores about 100,000 a second, so that is 350 GPUs fully busy, about 600 at 60% busy. At $2 an hour, about $29,000 a day, or 1.4 cents per 1,000 pages.

So we treat the big model's input size as a budget. Halving it from 500 to 250 halves the GPUs.

Small model210M scores/sabout 420 CPU cores

70,000 requests × 3,000 candidates, at about 2 microseconds each.

So we let a small CPU model see every candidate, so the GPU model sees only 500.

Feature reads35M rows/sat peak

The big model needs a row for each of its 500 videos, plus the user and session rows. One key per feature would be 100 times more reads.

So we store features as one row per video and one per user, read in one batch per request.

Events230,000/s690,000/s at peak, 4 TB a day

About one event per video viewed (watch time, finished, skipped), plus likes and follows, at about 200 bytes each.

So we put events on a stream split by user, and keep 90 days for training.

Notice that feature reads and events are as big a job as the model. The design has to plan for all three.

Go deeperThe calculator, and the size of the search index2 min
Change an assumption
Feed requests, average
23,000/s
Feed requests, peak
69,000/s
Stateless feed servers scale out; the cost is in what each request does.
Light ranker scores, peak
210,000,000/s
At about 2 µs a score on CPU, a few hundred cores. Cheap per item, so it can see every candidate.
Heavy ranker scores, peak
35,000,000/s
GPUs for the heavy ranker, fully busy
350
Provision about 1.7× this to run near 60% busy. The heavy ranker is the biggest serving cost.
Feature rows read, peak
35,000,000/s
A large in-memory cluster can serve this if features are packed into one row per video and per user, never one key per feature.
Events (views, skips, likes), average
230,000/s
Events, peak
690,000/s
One event stream, partitioned by user, so one consumer owns each session.
Raw events kept
360 TB
Columnar files in object storage. Training reads a sample of negatives, not all of it.
Retrieval index in memory
72 GB
Fits in one large machine compressed. Shard it anyway, for query load and latency.

The search index. Each video is turned into 128 numbers. As 32-bit numbers, that is 512 bytes, or 512 GB for the catalog. Compression (see vector search) cuts each to 64 bytes plus an 8-byte ID: 72 GB. We split it 8 ways for query load, not for size.

We start with a trending list. Then each step fixes the biggest problem left. The drawing grows with each step.

Interviewerminute 11

Start simple. What’s the first version you’d ship?

What they are testing

Whether you can fill a feed with no model in two minutes, and name what it gets wrong. Starting with a two-tower diagram skips the baseline every later model is compared against.

An hourly job counts views and finishes per video per region, from the event stream. The feed service pages through the region’s top 500, skipping what the user has seen. That is a working feed for every user on day one, and the fallback for the rest of the page.

Video recommendation feed · architecture

Stage 1: A trending feed

UsersAPI gatewayauth, route by userFeed servicefilters seen videos59 servers, 59% busyTrending liststop 500 per regionEvent streamviews, skips, likes23% busyPopularity jobhourly view counts
Color keyServiceDatabaseCacheQueue or streamBackground workerModelNew in this stageAsync or occasional

Stage 1 of 7

A trending feed

Why this stage

Nothing yet.

This is the smallest design that fills a feed for every user on day one.

What we add

A feed service that pages through a per-region trending list, minus what the user has seen, and an hourly job that builds the lists from view events.

What it costs

Everyone in a region sees the same feed, a video needs hours to trend, and a new upload with no views can never get in.

At 70k feed requests per second, every part has headroom. Raise the traffic until something turns red.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
The numbers behind the busy bars

How busy it gets

  • Each feed request produces about 10 events.

What one machine can do

  • One feed server handles about 2,000 requests a second.
  • One ANN replica set (8 shards) answers about 5,000 queries a second.
  • One GPU makes about 100,000 heavy-ranker scores a second. That is 200 requests a second at 500 videos each, or 400 at 250.

How big the shared systems are

  • The feature store is sized for 150,000 requests a second of about 500 rows each. Caching video rows on the ranker hosts lets it serve ten times that.
  • The event stream takes 3 million events a second, and 10 million with client batching and more partitions.
  • Instance counts aim for 60% busy.
TrendingList.javatrending_list.pyTrendingList.cstrending_list.gotrending-list.ts
// Stage 1: one trending list per region, rebuilt every hour from view counts. Everyone in a
// region gets the same order, minus the videos they have already seen.
class TrendingList {
// After 6 hours a view counts half, so last night's hit fades instead of staying on top.
private static final double HALF_LIFE_HOURS = 6.0;
private static final double DROP_BELOW = 1.0; // keeps the map to videos watched recently
private final Map<String, Double> score = new HashMap<>();
void addHour(Map<String, Integer> views, Map<String, Integer> completions) {
// One number per video, decayed once an hour, instead of keeping every view.
double decay = Math.pow(0.5, 1.0 / HALF_LIFE_HOURS);
score.replaceAll((id, s) -> s * decay);
views.forEach((id, n) -> {
// A completion counts five times a start. Ranking by starts alone rewards
// clickbait: a thumbnail that gets the tap and a video that loses you in a second.
double add = 0.2 * n + completions.getOrDefault(id, 0);
score.merge(id, add, Double::sum);
});
score.values().removeIf(s -> s < DROP_BELOW);
}
List<String> top(int k) {
return score.entrySet().stream()
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
.limit(k)
.map(Map.Entry::getKey)
.toList();
}
// The only personal touch stage 1 has: never show the same person a video twice.
static List<String> page(List<String> trending, Set<String> seen, int size) {
return trending.stream().filter(id -> !seen.contains(id)).limit(size).toList();
}
}

Its problems are the reasons for the next steps. Everyone in a region sees the same feed. A video needs an hour of views to trend, so the list is always behind. And a new upload with no views can never get in.

Interviewerminute 13

Make it personal. There’s a billion videos. How do you find the few hundred worth considering for this user?

What they are testing

Whether you separate finding candidates from ranking them, and know why: first don’t miss good ones, then order them. “Run the model over all videos” with no math is the red flag.

Scoring a billion videos per request is impossible. So the first job is to find a few thousand plausible ones, fast. This is called retrieval. We judge it by one question: did the good videos make it into the set at all?

The main source is a two-tower model. It has two halves:

Two towers meet in one space

User: history, language, time of day

User tower

128 numbers

Video: topic, creator, frames, captions

Video tower

128 numbers, computed ahead for every video

Search: the 2,000 closest videos

User: history, language, time of day

User tower

128 numbers

Video: topic, creator, frames, captions

Video tower

128 numbers, computed ahead for every video

Search: the 2,000 closest videos

Scroll or pinch to zoom, drag to pan
  • Color key
  • Inputs
  • Models (towers)
  • Vectors: 128 numbers
  • Search

The two towers are trained together, so a user lands close to the videos they finished. Video vectors are computed ahead into a search index. Per request, we run the user tower and ask the index for the 2,000 closest videos. The index finds them in milliseconds because it searches approximately. This is called approximate nearest-neighbor search, or ANN. And because the video tower reads content, a new video gets a sensible vector before anyone watches it.

Two more sources cover what the towers miss: new uploads from followed creators (500), and the trending list (500). A merger removes duplicates and seen videos, and caps each source so one can’t crowd out the rest.

Video recommendation feed · architecture

Stage 2: Personal candidates

UsersAPI gatewayauth, route by userFeed servicemerges sources59 servers, 59% busyTrending liststop 500 per regionEvent streamviews, skips, likes23% busyPopularity jobhourly view countsUser towerhistory → 128-d vectornewRetrieval serviceembedding + follownewANN index1B videos, 8 shardsnew24 servers, 58% busyFollow graphwho follows whomnewTrainingmodels, batch featuresnewLog warehouseeventsnew
Color keyServiceDatabaseCacheQueue or streamBackground workerModelNew in this stageAsync or occasional
Stage

Stage 2 of 7

Personal candidates

Why this stage

One list for 200 million people wastes most of their watch time, and the index holds 1 billion videos: nothing can score them all per request.

What we add

A retrieval service that runs a user tower and an ANN search over video embeddings, a follow-graph source, the trending list as a third source, and a daily training pipeline over logged events.

What it costs

Candidates come back unordered and of mixed quality, and the embeddings are a day old.

At 70k feed requests per second, every part has headroom. Raise the traffic until something turns red.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
The numbers behind the busy bars

How busy it gets

  • Each feed request produces about 10 events.

What one machine can do

  • One feed server handles about 2,000 requests a second.
  • One ANN replica set (8 shards) answers about 5,000 queries a second.
  • One GPU makes about 100,000 heavy-ranker scores a second. That is 200 requests a second at 500 videos each, or 400 at 250.

How big the shared systems are

  • The feature store is sized for 150,000 requests a second of about 500 rows each. Caching video rows on the ranker hosts lets it serve ten times that.
  • The event stream takes 3 million events a second, and 10 million with client batching and more partitions.
  • Instance counts aim for 60% busy.
CandidateMerger.javacandidate_merger.pyCandidateMerger.cscandidate.gocandidate-merger.ts
// One video a source proposes. The source travels with it: the ranker uses it as a feature.
record Candidate(String videoId, String creatorId, String source) {}
// Stage 2: several sources propose videos. The merger drops duplicates and anything the user
// has seen or must not see, and caps each source so one cannot crowd out the others.
class CandidateMerger {
// Counted after filtering, so a source full of seen videos still fills its share.
static final Map<String, Integer> QUOTA =
Map.of("follow", 500, "embedding", 2000, "trending", 500);
// When two sources propose the same video, the first one here gets the credit.
static final List<String> ORDER = List.of("follow", "embedding", "trending");
List<Candidate> merge(Map<String, List<Candidate>> bySource,
Predicate<String> seen, Set<String> blockedCreators) {
List<Candidate> out = new ArrayList<>();
Set<String> taken = new HashSet<>();
for (String source : ORDER) {
// A source that timed out is simply missing: the page is built from the rest.
int kept = 0;
for (Candidate c : bySource.getOrDefault(source, List.of())) { // best first
if (kept == QUOTA.get(source)) break;
// `seen` is a Bloom filter. It wrongly says "seen" for about 1% of videos,
// which costs a candidate; it never says "not seen" for a video already shown.
if (seen.test(c.videoId()) || blockedCreators.contains(c.creatorId())) continue;
if (!taken.add(c.videoId())) continue;
out.add(c);
kept++;
}
}
return out;
}
}

The towers are retrained daily, and the whole catalog is re-embedded. The user tower and the index must come from the same training run, or the vectors live in different spaces and the search returns noise. So they ship together, as one versioned pair.

Interviewerminute 16

You have 3,000 candidates and room for 10. How do you order them?

What they are testing

Whether you size the ranking steps from the time budget, predict several outcomes, not one, and know where the features come from at serving time. “A deep network predicts watch time” with no word on features or cost is a mid-level answer.

Ranking runs in two steps:

  • A small model with a few dozen cheap features scores all 3,000 on CPUs in about 6 ms, and keeps 500.
  • A big model with hundreds of features scores those 500 on a GPU in about 25 ms. 500 is what 600 GPUs can afford at 70,000 requests a second.

The big model predicts several things at once: will the user finish the video, watch for a few seconds, like, share, follow, or tap “not interested”? A value model combines the predictions into one score, with weights the product team chooses. Those weights decide what the feed is for.

ValueModel.javavalue_model.pyValueModel.csweights.govalue-model.ts
// What the heavy ranker predicts for one user and one video: several heads, one network.
record Predictions(double pComplete, double watchSeconds, double pLike, double pShare,
double pFollow, double pNotInterested) {}
// Chosen by the product team, versioned and A/B tested like a model, because these numbers
// decide what the feed optimizes for.
record Weights(double watch, double complete, double like, double share, double follow,
double notInterested) {}
// Stage 3: turns several predictions into the one number the feed sorts by.
class ValueModel {
// Watch time alone favors long videos: a 3 minute video watched halfway beats a 15 second
// one watched twice. Capping the seconds stops length from winning on its own.
private static final double WATCH_CAP_S = 60;
private final Weights w;
ValueModel(Weights w) { this.w = w; }
// A weighted sum of probabilities only means something if each one is calibrated
// (a predicted 0.3 happens 30% of the time). The release gate checks that per head.
double score(Predictions p) {
double watch = Math.min(p.watchSeconds(), WATCH_CAP_S) / WATCH_CAP_S;
return w.watch() * watch
+ w.complete() * p.pComplete()
+ w.like() * p.pLike()
+ w.share() * p.pShare()
+ w.follow() * p.pFollow()
// Rare but strong: a small chance of "not interested" with a large weight
// keeps a video this user would hide off the page.
- w.notInterested() * p.pNotInterested();
}
}

The features come from a feature store: one row per user, per video and per session, read in one batch. Every page served is logged with the exact features and model versions used. So the training data is exactly what the model saw.

Video recommendation feed · architecture

Stage 3: A ranking model

UsersAPI gatewayauth, route by userFeed servicemerges sources59 servers, 59% busyTrending liststop 500 per regionEvent streamviews, skips, likes23% busyPopularity jobhourly view countsUser towerhistory → 128-d vectorRetrieval serviceembedding + followRanking servicelight + heavy (GPU)new584 servers, 60% busyANN index1B videos, 8 shards24 servers, 58% busyFollow graphwho follows whomFeature storeuser and video rowsnew47% busyTrainingmodels, batch featuresLog warehouse+ served features
Color keyServiceDatabaseCacheQueue or streamBackground workerModelNew in this stageAsync or occasional
Stage

Stage 3 of 7

A ranking model

Why this stage

About 3,000 candidates arrive per request and the page shows 10.

Their order decides watch time, and it needs hundreds of features no retrieval model can use.

What we add

A ranking service (a light ranker on CPU, a multi-task heavy ranker on GPUs), an online feature store, and a log of the exact features served.

What it costs

About 600 GPUs at peak, and a feature store on the critical path of every request.

At 70k feed requests per second, every part has headroom. Raise the traffic until something turns red.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
The numbers behind the busy bars

How busy it gets

  • Each feed request produces about 10 events.

What one machine can do

  • One feed server handles about 2,000 requests a second.
  • One ANN replica set (8 shards) answers about 5,000 queries a second.
  • One GPU makes about 100,000 heavy-ranker scores a second. That is 200 requests a second at 500 videos each, or 400 at 250.

How big the shared systems are

  • The feature store is sized for 150,000 requests a second of about 500 rows each. Caching video rows on the ranker hosts lets it serve ten times that.
  • The event stream takes 3 million events a second, and 10 million with client batching and more partitions.
  • Instance counts aim for 60% busy.
Go deeperOne page, step by step, and the data model3 min
One feed page: candidates, then two models
Event streamFeature storeRanking serviceRetrievalFeed serviceAppEvent streamFeature storeRanking serviceRetrievalFeed serviceApppar[candidate sources, each with a deadline]GET /v1/feed (cursor)seen filter + session rowtower + follow candidatesabout 2,500trending list from memorymerge, remove duplicates and seen: about 3,000rank 3,000small model on CPU: keep 500batch read 500 video rows + user rowbig model on GPU: 6 predictions each500 scoredvalue model, take the top10 videos + requestIdlog of what was shown, with the features usedEvent streamFeature storeRanking serviceRetrievalFeed serviceAppEvent streamFeature storeRanking serviceRetrievalFeed serviceApppar[candidate sources, each with a deadline]GET /v1/feed (cursor)seen filter + session rowtower + follow candidatesabout 2,500trending list from memorymerge, remove duplicates and seen: about 3,000rank 3,000small model on CPU: keep 500batch read 500 video rows + user rowbig model on GPU: 6 predictions each500 scoredvalue model, take the top10 videos + requestIdlog of what was shown, with the features used
Scroll or pinch to zoom, drag to pan
  • Color key
  • Queue or stream
  • Database
  • Service

The data. Almost every read on the request path is a batch of rows by key.

-- Feature store: one packed row per thing, in memory
video:{id} views_1h, views_24h, completion_rate_24h, like_rate_24h, skip_rate_24h,
age_minutes, content_emb[64] written by stream + batch
user:{id} long_term_emb[128], topic_affinity{}, creator_affinity{}, language, country
written daily by batch
session:{id} topic_affinity{} (fades over 5 min), recent_videos[50], skips_by_creator{}
written by stream, gone after 30 min
seen:{user_id} a compact filter of videos shown in the last 30 days, about 4 KB
-- Training logs: column files in object storage, split by hour
impressions request_id, user_id, video_id, position, source, model_versions,
features (exactly as served), served_at
events event_id, request_id, video_id, type, watch_ms, completed, ts

The seen filter is a Bloom filter, a compact set that can say “maybe seen” by mistake, but never “not seen” by mistake. It may wrongly hide 1% of candidates, but it never lets a seen video through.

The feature store is an in-memory key-value store, like Redis, so 500 rows come back in about 10 ms. Training reads cheap files in Amazon S3 without touching serving.

Interviewerminute 19

I skip three cooking videos in a row. When does the feed notice?

What they are testing

Whether session features come from a stream and reach the next page within seconds, and whether you handle repeated and late events. “We retrain the model every hour” is the red flag. The model doesn’t need to change. Its inputs do.

With daily features, the answer is “tomorrow”. A session lasts minutes. The fix is fresher inputs, not a faster model.

A stream processor reads the event stream. The stream is split by user, so one worker owns each session. It keeps a session row: how much the user likes each topic right now (fading over about 5 minutes), the last 50 videos, and skips per creator. The next page’s ranking reads it like any other feature. The same stream keeps hourly counts per video, so a video going viral now ranks higher now.

A skip changes the next page
Feed serviceFeature storeStream processorEvent streamGatewayAppFeed serviceFeature storeStream processorEvent streamGatewayAppabout 0.6 s after the swipeskip after 1.2 s (eventId e1)append, key = user idbatch, about every 0.5 ssession row: cooking down, creator skip +1GET next pageread session row with the user rowpage ranked with cooking pushed downe1 again after a worker restarte1 already applied: ignoredFeed serviceFeature storeStream processorEvent streamGatewayAppFeed serviceFeature storeStream processorEvent streamGatewayAppabout 0.6 s after the swipeskip after 1.2 s (eventId e1)append, key = user idbatch, about every 0.5 ssession row: cooking down, creator skip +1GET next pageread session row with the user rowpage ranked with cooking pushed downe1 again after a worker restarte1 already applied: ignored
Scroll or pinch to zoom, drag to pan
  • Color key
  • Service
  • Database
  • Queue or stream
Video recommendation feed · architecture

Stage 4: Real-time signals

UsersAPI gatewayauth, route by userFeed servicemerges sources59 servers, 59% busyTrending liststop 500 per regionEvent streamviews, skips, likes23% busyStream processorsession rows, counters24 servers, 58% busyUser towerhistory → 128-d vectorRetrieval serviceembedding + followRanking servicelight + heavy (GPU)584 servers, 60% busyANN index1B videos, 8 shards24 servers, 58% busyFollow graphwho follows whomFeature store+ session rows47% busyTrainingmodels, batch featuresLog warehouse+ served features
Color keyServiceDatabaseCacheQueue or streamBackground workerModelNew in this stageAsync or occasional
Stage

Stage 4 of 7

Real-time signals

Why this stage

Features refreshed daily cannot see that the user skipped three cooking videos a minute ago, and a video going viral now looks cold until tomorrow.

What we add

A stream processor that turns events into session features and video counters in the online store within about a second.

What it costs

A stream job to run and keep correct, with duplicate and late events to handle.

At 70k feed requests per second, every part has headroom. Raise the traffic until something turns red.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
The numbers behind the busy bars

How busy it gets

  • Each feed request produces about 10 events.

What one machine can do

  • One feed server handles about 2,000 requests a second.
  • One ANN replica set (8 shards) answers about 5,000 queries a second.
  • One GPU makes about 100,000 heavy-ranker scores a second. That is 200 requests a second at 500 videos each, or 400 at 250.

How big the shared systems are

  • The feature store is sized for 150,000 requests a second of about 500 rows each. Caching video rows on the ranker hosts lets it serve ten times that.
  • The event stream takes 3 million events a second, and 10 million with client batching and more partitions.
  • Instance counts aim for 60% busy.
SessionState.javasession_state.pySessionState.cssession_state.gosession-state.ts
record WatchEvent(String eventId, String videoId, List<String> topics, long watchMs,
boolean completed, boolean liked, long tsMs) {}
// Stage 4: kept per active session by the stream processor, in the online feature store. The
// ranker reads it on the next page, so a skip a second ago changes what comes next.
class SessionState {
private static final double HALF_LIFE_MS = 5 * 60_000; // a session's mood shifts fast
private static final long QUICK_SKIP_MS = 2_000; // swiped away before it got going
private static final int REMEMBER = 200; // event ids kept for dedup
private final Map<String, Double> topicAffinity = new HashMap<>();
private final Deque<String> recentIds = new ArrayDeque<>();
private final Set<String> recentIdSet = new HashSet<>();
private long asOfMs;
void apply(WatchEvent e) {
// The stream delivers at least once, so an event can arrive twice. Counting a skip
// twice would push its topics down twice as hard.
if (!recentIdSet.add(e.eventId())) return;
recentIds.addLast(e.eventId());
if (recentIds.size() > REMEMBER) recentIdSet.remove(recentIds.removeFirst());
// Decay lazily, only when the session changes, so there is no timer per session. A
// late event is applied without moving the clock backwards.
if (e.tsMs() > asOfMs) {
double f = Math.pow(0.5, (e.tsMs() - asOfMs) / HALF_LIFE_MS);
topicAffinity.replaceAll((topic, v) -> v * f);
asOfMs = e.tsMs();
}
// A quick skip is the strongest negative signal a feed gets without asking.
double signal = e.watchMs() < QUICK_SKIP_MS ? -1.0
: (e.completed() ? 1.0 : 0.3) + (e.liked() ? 1.0 : 0.0);
for (String t : e.topics()) topicAffinity.merge(t, signal, Double::sum);
}
// Read by the ranker as features. A copy, so a concurrent update never tears a read.
Map<String, Double> features() { return Map.copyOf(topicAffinity); }
}

The stream may deliver an event twice. So the update ignores an event ID it has already applied (this is called idempotency). The fading is computed only when a new event arrives, so millions of idle sessions cost nothing.

Interviewerminute 22

Your top 10 has three videos by the same creator, and nothing uploaded today. Fix both.

What they are testing

Whether you treat the page as a set, not 10 separate scores, and see testing new videos as a budget that pays for future quality. “Add some randomness”, without saying how much, where and what it costs, is the red flag.

The big model scores each video alone, so similar high scores cluster. A final pass builds the page as a whole:

  • At most one video per creator.
  • A penalty for each repeated topic.
  • Age and country rules.
  • 5% of slots go to new videos, from uploads under 48 hours old that match the user’s topics. Never the first two slots, because those decide whether the user keeps scrolling. Testing new things like this is called exploration.
Video recommendation feed · architecture

Stage 5: Re-ranking and exploration

UsersAPI gatewayauth, route by userFeed service+ re-rank, explore59 servers, 59% busyTrending liststop 500 per regionEvent streamviews, skips, likes23% busyStream processorsession rows, counters24 servers, 58% busyUser towerhistory → 128-d vectorRetrieval serviceembedding + followRanking servicelight + heavy (GPU)584 servers, 60% busyNew video poolunder 48 h, by topicnewANN index1B videos, 8 shards24 servers, 58% busyFollow graphwho follows whomFeature store+ session rows47% busyTrainingmodels, batch featuresLog warehouse+ served features
Color keyServiceDatabaseCacheQueue or streamBackground workerModelNew in this stageAsync or occasional
Stage

Stage 5 of 7

Re-ranking and exploration

Why this stage

The ranker scores each video alone, so a page can hold three videos by one creator, and 10 million uploads a day get no impressions without a way in.

What we add

A re-rank step that builds the page as a whole (creator caps, topic spread, policy) and spends 5% of slots on new videos from a fresh pool.

What it costs

A little short-term watch time in exchange for a catalog that keeps renewing itself.

At 70k feed requests per second, every part has headroom. Raise the traffic until something turns red.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
The numbers behind the busy bars

How busy it gets

  • Each feed request produces about 10 events.

What one machine can do

  • One feed server handles about 2,000 requests a second.
  • One ANN replica set (8 shards) answers about 5,000 queries a second.
  • One GPU makes about 100,000 heavy-ranker scores a second. That is 200 requests a second at 500 videos each, or 400 at 250.

How big the shared systems are

  • The feature store is sized for 150,000 requests a second of about 500 rows each. Caching video rows on the ranker hosts lets it serve ten times that.
  • The event stream takes 3 million events a second, and 10 million with client batching and more partitions.
  • Instance counts aim for 60% busy.
ReRanker.javare_ranker.pyReRanker.csscored.gore-ranker.ts
record Scored(String videoId, String creatorId, String topic, double score) {}
// Stage 5: the heavy ranker scores each video alone. This builds the page as a whole: no
// creator twice, topics spread out, and some slots for new videos still being tested.
class ReRanker {
private static final double TOPIC_PENALTY = 0.15; // each repeat of a topic costs 15%
// The first two slots are never exploration: they decide whether the user keeps scrolling.
private static final int PROTECTED = 2;
private final double exploreRate; // share of all slots, 0.05 on this page
private final Random rng;
ReRanker(double exploreRate, Random rng) {
this.exploreRate = exploreRate;
this.rng = rng;
}
List<Scored> page(List<Scored> ranked, Deque<Scored> fresh, int size) {
List<Scored> pool = new ArrayList<>(ranked);
List<Scored> page = new ArrayList<>();
Set<String> creators = new HashSet<>();
Map<String, Integer> topics = new HashMap<>();
// Spread the exploration share over the slots allowed to hold it, so the overall
// rate still comes out at exploreRate.
double pExplore = exploreRate * size / (size - PROTECTED);
while (page.size() < size) {
Scored pick = null;
if (page.size() >= PROTECTED && !fresh.isEmpty() && rng.nextDouble() < pExplore) {
pick = fresh.pollFirst(); // a new video, already matched to this user's topics
} else {
// Greedy: score what is left against what the page already holds. 10 slots
// times 500 videos is nothing next to the ranker.
double best = Double.NEGATIVE_INFINITY;
for (Scored s : pool) {
if (creators.contains(s.creatorId())) continue;
int repeats = topics.getOrDefault(s.topic(), 0);
double adjusted = s.score() * Math.pow(1 - TOPIC_PENALTY, repeats);
if (adjusted > best) {
best = adjusted;
pick = s;
}
}
if (pick == null) break; // a short page beats a repetitive one
pool.remove(pick);
}
page.add(pick);
creators.add(pick.creatorId());
topics.merge(pick.topic(), 1, Integer::sum);
}
return page;
}
}

This is enough for a mid-level answer. Trending as a fallback, two towers to find candidates, two models to rank them, a stream for session signals, and a final pass for variety and new videos. The rest of the page is what makes it a senior answer: proving quality, the size of the exploration budget, ten times the users, and hidden bugs in training.

Go deeperFollow a page and a skip, and the classes3 min

Follow one full page. Use Next hop to move along. The bar is the server time budget.

Video recommendation feed · architecture

Trace a request: Next feed page

UsersAPI gatewayauth, route by userFeed service+ re-rank, exploreTrending liststop 500 per regionEvent streamviews, skips, likesStream processorsession rows, countersUser towerhistory → 128-d vectorRetrieval serviceembedding + followRanking servicelight + heavy (GPU)New video poolunder 48 h, by topicANN index1B videos, 8 shardsFollow graphwho follows whomFeature store+ session rowsTrainingmodels, batch featuresLog warehouse+ served features
Color keyServiceDatabaseCacheQueue or streamBackground workerModelNew in this stageAsync or occasionalHop in motionHops doneResponse

Next feed page: step 1 of 12

Users to API gateway

The app asks for the next page while the user is still three videos from the end of this one. The gateway checks the token and routes by user id.

3 ms of a 200 ms budget
  1. Users → API gateway3 ms
  2. API gateway → Feed service2 ms
  3. Feed service → Retrieval service2 ms
  4. Retrieval service → User tower6 ms
  5. Retrieval service → ANN index15 ms
  6. Retrieval service → Follow graph0 ms
  7. Feed service → Trending lists0 ms
  8. Feed service → Ranking service6 ms
  9. Ranking service → Feature store35 ms
  10. Feed service → New video pool3 ms
  11. Feed service → Event streamasync
  12. Response
Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.

And one skip, from the swipe to the session row:

Video recommendation feed · architecture

Trace a request: A skip reaches the next page

UsersAPI gatewayauth, route by userFeed servicemerges sourcesTrending liststop 500 per regionEvent streamviews, skips, likesStream processorsession rows, countersUser towerhistory → 128-d vectorRetrieval serviceembedding + followRanking servicelight + heavy (GPU)ANN index1B videos, 8 shardsFollow graphwho follows whomFeature store+ session rowsTrainingmodels, batch featuresLog warehouse+ served features
Color keyServiceDatabaseCacheQueue or streamBackground workerModelNew in this stageAsync or occasionalHop in motionHops doneResponse

A skip reaches the next page: step 1 of 5

Users to API gateway

The user swipes away after 1.2 seconds. Skips, likes and "not interested" are sent at once; plain view progress is batched every few seconds. Each event has an id.

100 ms of a 2000 ms budget
  1. Users → API gateway100 ms
  2. API gateway → Event stream20 ms
  3. Event stream → Stream processor500 ms
  4. Stream processor → Feature store5 ms
  5. Response
Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
The feed service's classes

fans out to

small, then big

session rows

FeedService

+page(user, cursor) : Page

«interface»

CandidateSource

+fetch(user, limit, deadline) : List<Candidate>

EmbeddingSource

FollowSource

TrendingSource

CandidateMerger

+merge(bySource, seen, blocked) : List<Candidate>

«interface»

Ranker

+score(user, candidates) : List<Scored>

LightRanker

HeavyRanker

ValueModel

+score(Predictions) : double

ReRanker

+page(ranked, fresh, size) : List<Scored>

«interface»

FeatureStore

+rows(keys) : Map

SessionState

+apply(WatchEvent)

«value»

Candidate

videoId

creatorId

source

«value»

Weights

fans out to

small, then big

session rows

FeedService

+page(user, cursor) : Page

«interface»

CandidateSource

+fetch(user, limit, deadline) : List<Candidate>

EmbeddingSource

FollowSource

TrendingSource

CandidateMerger

+merge(bySource, seen, blocked) : List<Candidate>

«interface»

Ranker

+score(user, candidates) : List<Scored>

LightRanker

HeavyRanker

ValueModel

+score(Predictions) : double

ReRanker

+page(ranked, fresh, size) : List<Scored>

«interface»

FeatureStore

+rows(keys) : Map

SessionState

+apply(WatchEvent)

«value»

Candidate

videoId

creatorId

source

«value»

Weights

Scroll or pinch to zoom, drag to pan
  • Color key
  • Facade
  • Entity
  • Value object
  • Enum
  • Interface
  • Implementation

A new candidate source or ranker is a new class, not an edit to the feed service.

The API. GET /v1/feed?cursor=... returns 10 videos and a requestId. The cursor holds the session and a page number, not a list of videos, so each page is ranked fresh. POST /v1/events reports what the user did. Each event carries the requestId and the video’s position, so training can tie a later skip to the exact ranking decision. Each event also has its own ID, so a retry is safe.

Interviewerminute 25

You have a new ranking model. How do you know it’s better, and what stops a bad one from shipping?

What they are testing

Whether you know offline metrics only pick models for an A/B test, and you name guardrails and position bias. “AUC went up, ship it” is the red flag. Offline logs only contain what the old model chose to show.

Here the labels are what users did, not labels written by people. The check still runs in two parts:

  • Offline, on yesterday’s logs. Does the model rank the videos people finished above the ones they skipped? Do its predictions match what really happened? These tests only pick which models deserve a live test.
  • Online, in an A/B test for two weeks, on 1% of users per group. Watch time per user must win. The guardrails (“not interested”, reports, and users still active after 7 days) must not get worse by more than 1%.

Why offline isn’t enough. The logs contain only videos the old model chose. A new model that likes different videos is graded on a test it can’t pass. The videos it would have shown were never shown, so they have no labels.

Position bias. A video in slot 1 gets watched more because it is in slot 1. During training, the model learns position separately. At serving time, position is set to the same value for every video, so the model ranks the video, not the slot.

A long-term holdout keeps 1% of users on last quarter’s models. It shows whether many small wins add up, or slowly wear the feed down.

Go deeperAll the release metrics2 min
Metric How we measure it Target Blocks a release?
Retrieval recall at 3,000 Replay yesterday’s requests. Share of next-day finished videos found No drop over 1% Yes
AUC per prediction Next-day logs the model never trained on Finish prediction: no drop over 0.002 Yes
Calibration per prediction Predicted against real rate, in every score range Within ±5% everywhere Yes
NDCG at 10 Re-rank logged sessions. Finished videos count as relevant No drop over 0.5% Yes
Watch time per daily user A/B test, 2 weeks, 1% of users per group Wins at 95% confidence Yes: the main one
7-day retention The same A/B, plus the long-term holdout No drop over 1% Yes
“Not interested” and reports The same A/B No rise over 1% Yes: guardrail
New videos reach 200 views Eligible uploads within 24 hours 95% or more Yes
Server time per page Shadow traffic before the A/B Under 200 ms for 99% Yes

AUC measures how often the model ranks a finished video above a skipped one. NDCG measures whether the best videos sit near the top of the page. Calibration means a predicted 30% chance really happens about 30% of the time, which the weighted sum needs.

The release runs in order, and each step can stop it: offline metrics, then the model scores live traffic without being used, then 1% of traffic for a day, then the two-week A/B.

Interviewer asksWatch time is up 2% but 'not interested' is up 3%. Ship it?

No. Guardrails exist for this case. The model found videos people watch and regret, and users usually leave weeks later. Look at what changed. Often one prediction is off, or the weights reward length. Fix it, and test again.

The design works at today’s size. Now we push it through four hard situations: how much to explore, a new user and a new video, ten times the users, and a model that wins offline and loses online.

Interviewerminute 29

Your ranker learns from what it showed. Isn’t that a loop? And how much exploration is enough?

What they are testing

Whether you can name the bias and size exploration with math, not taste. “A few percent” with no reasoning is weak. Deriving 5% from uploads and test size is strong.

The model only gets labels for videos it chose to show. A video it underrates is never shown, so the mistake is never corrected, and the feed narrows. This is called selection bias. The cure is to show some videos the model is unsure about.

The budget is math. The feed shows 20 billion videos a day. About half of the 10 million daily uploads pass the quality checks. If judging a video takes about 200 views, testing them all costs 5 million × 200 = 1 billion views. That is 5% of slots. Try other values:

Ten feed pages, ten slots each. Amber slots go to new videos.

New videos given a fair test, per day
Short-term watch time, against no exploration
Exploration slots earn less today than the ranker's own picks.
Future hits found per day

Fixed: 20 billion feed slots and 10 million uploads a day. Illustrative assumptions: an exploration slot earns about 70% of the watch time of a ranked slot, and about 1 in 100 tested videos turns out better than the average ranked video.

If a test slot earns about 70% of a normal slot’s watch time, 5% costs about 1.5% of today’s watch time. (The 70% is an assumption. The A/B test measures the real number.) It buys a catalog that renews itself, and creators who keep uploading.

Interviewerminute 32

A user installs the app for the first time. Another uploads their first video. What does each see in the first hour?

What they are testing

Whether you treat the two as different problems with different fixes, and whether the models read features, not only IDs. “Show popular videos” for both is half an answer.

A new user has no history. But they have a device, a language, a country and a time of day. The first page is trending in their region and language. Then the session row does the work. By the fifth video, skips and finishes give the user tower something to work with. The tower reads features, not a user ID, so it can place a stranger.

A new video has no views, so nothing ranks it high, so it gets no views. Four things break that loop:

A new video's first day

people finish it

people skip it

Upload passes checks

Video tower turns it into 128 numbers, in minutes

A small fresh index, searched beside the main one

New video pool, under its topics

Test slots show it to about 200 matched users

Leaves the pool. Ranked like any video

Testing stops

people finish it

people skip it

Upload passes checks

Video tower turns it into 128 numbers, in minutes

A small fresh index, searched beside the main one

New video pool, under its topics

Test slots show it to about 200 matched users

Leaves the pool. Ranked like any video

Testing stops

Scroll or pinch to zoom, drag to pan
  • Color key
  • The upload
  • Model
  • Ready to be found
  • Test slots
  • Outcome

The fourth is a head start for creators whose videos usually do well. The test uses finishes and “not interested”, not taps, so a misleading thumbnail fails its test fast.

Interviewerminute 35

Users grow ten times. What breaks, and what does it cost?

What they are testing

Whether you scale from the numbers: GPUs, feature reads and events. “Add more servers” is true and useless. The strong answer cuts work per request, and says what each cut risks.

At 700,000 pages a second, the big model needs about 5,800 GPUs, about $100 million a year. The feature store and the event stream fall behind, and the search index needs over 230 copies. Cutting work per page is cheaper than buying all that.

Step 6: cheaper pages. Four cuts, each aimed at one number:

  1. Find candidates once per few pages, rank every page. The 3,000 candidates are kept for three pages. Ranking still runs on every page with fresh session signals, so a skip still counts. Search load falls to about a third.
  2. The big model sees 250 videos, not 500. First check offline how often the big model’s top 10 sit in the small model’s top 250. At 98% or more, the cut halves the GPUs and loses almost nothing.
  3. Popular video rows kept on the ranking servers. The hot 10 million video rows (about 10 GB) live in each server’s memory, refreshed every minute. The feature store then serves ten times the traffic with the same cluster.
  4. The app sends events in batches, and the stream gets more partitions.
Video recommendation feed · architecture

Stage 6: Cheaper pages at 10×

UsersAPI gatewayauth, route by userFeed service+ re-rank, explore59 servers, 59% busyTrending liststop 500 per regionCandidate cacheper session, 3 pagesnewEvent streamviews, skips, likes7% busyStream processorsession rows, counters24 servers, 58% busyUser towerhistory → 128-d vectorRetrieval serviceembedding + followRanking serviceheavy sees 250292 servers, 60% busyNew video poolunder 48 h, by topicANN index1B videos, 8 shards9 servers, 54% busyFollow graphwho follows whomFeature store+ session rows5% busyTrainingmodels, batch featuresLog warehouse+ served features
Color keyServiceDatabaseCacheQueue or streamBackground workerModelNew in this stageAsync or occasional
Stage

Stage 6 of 7

Cheaper pages at 10×

Why this stage

At 700,000 requests a second the feature store and the event stream pass 100%, and the heavy ranker would need about 5,800 GPUs.

What we add

A per-session candidate cache, a heavy ranker that sees 250 videos instead of 500, video feature rows cached on ranker hosts, and client-side event batching.

What it costs

Candidates up to three pages old, a funnel that must be checked for lost recall, and more caches to keep warm.

At 70k feed requests per second, every part has headroom. Raise the traffic until something turns red.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
The numbers behind the busy bars

How busy it gets

  • Each feed request produces about 10 events.

What one machine can do

  • One feed server handles about 2,000 requests a second.
  • One ANN replica set (8 shards) answers about 5,000 queries a second.
  • One GPU makes about 100,000 heavy-ranker scores a second. That is 200 requests a second at 500 videos each, or 400 at 250.

How big the shared systems are

  • The feature store is sized for 150,000 requests a second of about 500 rows each. Caching video rows on the ranker hosts lets it serve ten times that.
  • The event stream takes 3 million events a second, and 10 million with client batching and more partitions.
  • Instance counts aim for 60% busy.

Step 7: regions. A user in Jakarta shouldn’t cross the Pacific for every page. So every region runs the full serving path, including a copy of the 72 GB index. Training stays in one place, because it needs every region’s logs. It ships each day’s models and index everywhere as one versioned bundle.

Video recommendation feed · architecture

Stage 7: Regions

Every region runs this serving pathIndex replica, feature store, stream.Users are served in their home region.Training and the warehouse are central.UsersAPI gatewayauth, route by userFeed service+ re-rank, explore59 servers, 59% busyTrending liststop 500 per regionCandidate cacheper session, 3 pagesEvent streamviews, skips, likes7% busyStream processorsession rows, counters24 servers, 58% busyUser towerhistory → 128-d vectorRetrieval serviceembedding + followRanking serviceheavy sees 250292 servers, 60% busyNew video poolunder 48 h, by topicANN index1B videos, 8 shards9 servers, 54% busyFollow graphwho follows whomFeature store+ session rows5% busyTrainingmodels, batch featuresLog warehouse+ served features
Color keyServiceDatabaseCacheQueue or streamBackground workerModelNew in this stageAsync or occasional
Stage

Stage 7 of 7

Regions

Why this stage

A user in Jakarta should not pay 200 ms to reach Virginia, and one region is one failure domain for the whole feed.

What we add

The full serving path copied to every region, with its own index replica, feature store and stream.

Training stays central.

What it costs

Models and indexes shipped to every region, and a user who travels starts with a cold session.

At 70k feed requests per second, every part has headroom. Raise the traffic until something turns red.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
The numbers behind the busy bars

How busy it gets

  • Each feed request produces about 10 events.

What one machine can do

  • One feed server handles about 2,000 requests a second.
  • One ANN replica set (8 shards) answers about 5,000 queries a second.
  • One GPU makes about 100,000 heavy-ranker scores a second. That is 200 requests a second at 500 videos each, or 400 at 250.

How big the shared systems are

  • The feature store is sized for 150,000 requests a second of about 500 rows each. Caching video rows on the ranker hosts lets it serve ten times that.
  • The event stream takes 3 million events a second, and 10 million with client batching and more partitions.
  • Instance counts aim for 60% busy.
Interviewerminute 39

The new ranker beat production offline by a wide margin, and lost the A/B test. What happened?

What they are testing

Whether “training and serving skew” is your first suspect, and you can say where the leak usually is. Blaming the A/B test, or retraining with more data, misses the most common cause.

The usual cause: the model trained on information it doesn’t have at serving time. This is called training and serving skew.

Here is a typical leak. A job computes “finish rate” from the full day’s logs. A video that went viral at 22:00 already looks viral at 09:00. Offline, the model looks brilliant, because it can see the future. Online, those values don’t exist yet.

The fix: log the features exactly as served, with each page. Training joins those logs with what the user did next. Then training sees exactly what the model saw.

Training examples from what was really served

Ranking: logs each page with its exact features

Join on request ID and video

What the user did: finished, skipped, liked

Training examples with no future leaks

Train, then the release tests

Ranking: logs each page with its exact features

Join on request ID and video

What the user did: finished, skipped, liked

Training examples with no future leaks

Train, then the release tests

Scroll or pinch to zoom, drag to pan
  • Color key
  • Logged when served
  • Joined data
  • What users did
  • Training

The cost: a new feature needs weeks of logging before the model can train on it. To speed that up, we rebuild a new feature’s past values “as of” each page’s time, only to fill the gap.

Go deeperA page in 200 ms for 99% of requests2 min

The median is 72 ms, but the slowest 1% take 350 ms. Where does the tail come from? A page waits for the slowest of many calls. If each of 8 index shards is slow 1% of the time, a query that needs all 8 is slow about 8% of the time. The fixes spend a little quality to cap the time:

Technique What it does Gives up
A deadline per source Each source gets 40 ms. The merger uses what arrived A few candidates on a bad request
Partial search results Merge the shards that answered in time One eighth of the catalog, that once
Hedged requests Ask a second copy if the first is slow About 5% more search load
GPU batch window Wait at most 5 ms to fill a batch, then run it Some GPU efficiency
A fallback ladder Big model late: use the small model’s order. Everything late: trending Quality on that page only

A slightly worse page is invisible to the user, and an error is not. So every step has a deadline, and a cheaper answer behind it. These are the usual resilience patterns.

Interviewerminute 42

The feature store is down. What does the user see?

What they are testing

Whether every part has a fallback that still returns a page, and whether you list quality failures (a bad model, broken labels) beside outages. Only listing server crashes is the red flag in a machine learning design.

The feed gets worse before it gets empty. The order of fallbacks is the design: the full pipeline, then the small model only, then saved candidates, then the trending list. If the feature store is down, the models use the default values they were trained with, and the small model’s order is served.

Click any box to break it and see what happens:

Video recommendation feed · architecture

Break it: what fails, who notices, what users see

Every region runs this serving pathIndex replica, feature store, stream.Users are served in their home region.Training and the warehouse are central.UsersAPI gatewayauth, route by userFeed service+ re-rank, exploreTrending liststop 500 per regionCandidate cacheper session, 3 pagesEvent streamviews, skips, likesStream processorsession rows, countersUser towerhistory → 128-d vectorRetrieval serviceembedding + followRanking serviceheavy sees 250New video poolunder 48 h, by topicANN index1B videos, 8 shardsFollow graphwho follows whomFeature store+ session rowsTrainingmodels, batch featuresLog warehouse+ served features
Color keyServiceDatabaseCacheQueue or streamBackground workerModelNew in this stageAsync or occasional
Break

Failure: click any box to break it

Feature store goes down

What breaks

The heavy ranker gets no user, session or video rows.

How we notice

Read error rate and p99 latency.

What users see

A less personal order if rows are missing; slow pages if reads hang.

What we do

A tight deadline on reads, defaults for missing features (the model was trained with some missing), and the light ranker fallback.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.

Some failures belong only to a learned system. No server is down, but the feed quietly gets worse:

  • Broken labels. An app release stops sending watch time. The model learns that every view was a skip. Training refuses data whose label rates move too far.
  • Tower and index mismatch. The user tower is from today and the index from yesterday. Search returns noise. They ship as one bundle, and a recall check runs on deploy.
  • The feed narrows over weeks. Fewer topics and creators per user. Exploration, variety rules and the long-term holdout catch it.
Go deeperEvery failure, and what we do2 min
Part Failure What users see What we do
Feature store Down or slow A slightly worse order Tight deadline. Default values. Small model’s order
Big model GPUs Overloaded or down A worse order Small model’s order, then saved candidates, then trending
Search index A shard down Fewer candidates Copies. Merge partial results
Event stream Down or behind The feed stops reacting in the session The last session row keeps serving. Replay safely
Training Fails Models age Alarm at 36 hours. Keep serving the last good model
A bad model Passes offline, hurts users Watch time or guardrails drop Canary and A/B guardrails. Roll back the bundle in minutes
Fake views Bots inflate a video Trending is poisoned Count only views from trusted sessions. Finishes count more
A whole region Outage A minute of errors, cold sessions DNS to the nearest region. Same model bundle everywhere

Here is everything we built, in the order we built it. Use the buttons to step through it.

Video recommendation feed · architecture

The full design, stage by stage

Every region runs this serving pathIndex replica, feature store, stream.Users are served in their home region.Training and the warehouse are central.UsersAPI gatewayauth, route by userFeed service+ re-rank, explore59 servers, 59% busyTrending liststop 500 per regionCandidate cacheper session, 3 pagesEvent streamviews, skips, likes7% busyStream processorsession rows, counters24 servers, 58% busyUser towerhistory → 128-d vectorRetrieval serviceembedding + followRanking serviceheavy sees 250292 servers, 60% busyNew video poolunder 48 h, by topicANN index1B videos, 8 shards9 servers, 54% busyFollow graphwho follows whomFeature store+ session rows5% busyTrainingmodels, batch featuresLog warehouse+ served features
Color keyServiceDatabaseCacheQueue or streamBackground workerModelNew in this stageAsync or occasional
Stage

Stage 7 of 7

Regions

Why this stage

A user in Jakarta should not pay 200 ms to reach Virginia, and one region is one failure domain for the whole feed.

What we add

The full serving path copied to every region, with its own index replica, feature store and stream.

Training stays central.

What it costs

Models and indexes shipped to every region, and a user who travels starts with a cold session.

At 70k feed requests per second, every part has headroom. Raise the traffic until something turns red.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.The whole design. Step back through the stages to rebuild it from memory.
The numbers behind the busy bars

How busy it gets

  • Each feed request produces about 10 events.

What one machine can do

  • One feed server handles about 2,000 requests a second.
  • One ANN replica set (8 shards) answers about 5,000 queries a second.
  • One GPU makes about 100,000 heavy-ranker scores a second. That is 200 requests a second at 500 videos each, or 400 at 250.

How big the shared systems are

  • The feature store is sized for 150,000 requests a second of about 500 rows each. Caching video rows on the ranker hosts lets it serve ten times that.
  • The event stream takes 3 million events a second, and 10 million with client batching and more partitions.
  • Instance counts aim for 60% busy.
Decision We chose We gave up We’d choose differently if
Approach Two towers to find candidates, then two ranking models The simplicity of popular lists The catalog were small enough to score in full
Goal Capped watch time and finishes, with guardrails The easy growth of optimizing taps The product sold ads per view
Funnel size 3,000 → 500 → 10 Some good videos the big model never sees GPUs were cheap
Freshness Session signals from a stream, daily training A model that adapts within the hour Trends turned over in minutes
Training data Log features as served Training on a new feature from day one Features changed weekly and logs cost too much
Exploration A fixed 5%, matched by topic, not in the first two slots About 1.5% of today’s watch time Uploads were few
At ten times Candidates kept for 3 pages, big model input 250 Candidates up to a minute old GPUs weren’t the limit
Regions Serve everywhere, train in one place Models per region Regions had very different laws or catalogs
  • 0 to 6. Scale, catalog, and above all the goal. Popular, co-watch, then the funnel. The token math that keeps an LLM out.
  • 6 to 11. The numbers: 70,000 pages a second, 3,000 candidates, 500 big-model scores, about 600 GPUs, 35 million feature rows a second.
  • 11 to 25. Build it: trending, two towers, two rankers, the stream, variety and new videos. A mid-level answer can stop here.
  • 25 to 29. Offline tests, the A/B test, guardrails, position bias.
  • 29 to 42. Stress it: exploration, cold start, ten times, skew.
  • 42 to 45. Failures, then your own questions.

Never skip the funnel math, the goal, or exploration.

Interviewer asksWhere could an LLM actually help?

Offline, around the funnel. It can write topics, captions and text summaries of new videos, which become better features for cold start. It can explain why a video was recommended, or clean up noisy labels. But not score candidates per request. The token math rules that out at this traffic.

Go deeperMore follow-up questions3 min

Why not one big model instead of finding candidates and then ranking? Cost per video. The big ranker mixes user and video features freely: accurate and expensive. The two towers meet only in one multiplication: weaker, but video vectors can be computed ahead and searched in milliseconds. The funnel uses each where it is affordable.

How often do you retrain, and could you train online? Daily, with streamed features, gets most of the value. Training online, updating the model from the stream every few minutes (as in ByteDance’s Monolith), catches trends faster. But a bad update reaches everyone in minutes, so it needs automatic checks and rollback first.

Product wants “show me fewer videos like this”. Where does it go? An event like any other, sent at once. The stream processor applies a strong negative to the video’s topics and creator in the session row, and a longer one in the user row. The big model’s “not interested” prediction already learns from it.

A video is taken down. How fast does it leave every feed? The takedown marks the video row removed. The merger, the new video pool and the final pass all filter on it, and ranking servers refresh cached rows every minute. The vector can stay in the index until its daily rebuild, because the filter runs after search. Worst case, about a minute.

How do you stop creators gaming the new video pool? Only uploads that pass checks, from accounts that aren’t flagged, get in, with a cap per creator per day. The test audience is matched by content, so an off-topic thumbnail gets skipped fast. Graduating uses finishes and “not interested”, not taps.

The seen filter is lost. What happens? Users see repeats for a while. Rebuild it from 30 days of logs. Meanwhile, the session row’s last 50 videos stop the most visible repeats.