Design Short Video Recommendation & Ranking
TikTok / Spotlight Feed ML System Design
Overview
Open TikTok or Instagram Reels and the next clip starts playing before you consciously chose it. That "for you" feeling is powered by a recommendation system that must pick the next few clips out of a catalog of hundreds of millions in well under 200 milliseconds, personalized to your taste, while optimizing for watch time and engagement — and doing this for hundreds of millions of users simultaneously.
This is a classic MLE / ML-system-design question, but it's equally fair game in SWE rounds on ranking, ads, or feed teams. The two audiences have different depths of focus:
- SWE interviewer: will push on the systems spine — how the request path from user to feed is structured, how a feature store is built, how scores are served at low latency, how logs flow to training data, what happens at scale.
- MLE interviewer: will then drill into the ML components — the model architecture, objective functions, how training data is constructed, why delayed labels matter, how you evaluate offline vs online, and how to handle feedback loops.
The system reduces millions of candidate videos to a ranked shortlist in two stages: a cheap retrieval stage (millions → hundreds) and a heavier ranking stage (hundreds → the final feed order). These two stages, the data loop that keeps them learning, and the serving infrastructure that hits the latency budget are the spine of a strong answer.
Out of scope (say so): video upload and transcoding pipeline, comments and social graph management, ads insertion and pacing (though mention it as a constraint), content moderation. We own only the feed recommendation and ranking path.
Functional requirements
- Personalized feed. Given a user opening the app (or swiping past the current video), return an ordered list of ~10–20 short video clips to play next, personalized to that user.
- Near-real-time freshness. New videos uploaded in the last few hours should be eligible for the feed, not just videos the system has seen for days.
- Multiple engagement signals. Rank videos to optimize a blend of watch time, likes, shares, follows, and comments — not just clicks.
- Cold start for new users and new videos. A brand-new user with no history, and a brand-new video with no engagement, must both be handleable.
- Record impressions and engagement. Log every video shown and every subsequent engagement event (watch time, like, share) to feed the training pipeline.
Out of scope (state it): video upload, transcoding, CDN delivery, abuse/spam filtering, comments, and ads auction. We assume videos are already transcoded and stored in a CDN.
Non-functional requirements
- Low latency. The feed response must be ready in <200ms end-to-end at p99. Because the video itself streams from CDN (fast), users perceive any lag in the metadata + recommendation response. Budget ~50ms for retrieval, ~60ms for ranking, ~30ms for feature assembly, and ~30ms for re-rank + network.
- High throughput. Hundreds of millions of daily active users, each swiping through many videos per session — peak feed QPS is enormous.
- Freshness over strong consistency. A slight delay (minutes) between a video upload and it appearing in feeds is acceptable. Eventual consistency on feature values is fine.
- Availability over precision. If the ranking model is slow, fall back to a simpler scoring rule; if a feature is missing, use defaults. A blank feed is far worse than a slightly suboptimal one.
- Reproducibility for debugging. Log enough context (user, candidates, features, scores) to reconstruct why a video was recommended — required for fairness audits and debugging feedback loops.
Estimations
State assumptions; the goal is to size the main subsystems, not to be exact.
Rounded assumptions
- Users: ~500M daily active users (DAU), each making ~10–20 feed requests per day.
- Videos in catalog: ~500M total; ~5M new videos uploaded per day.
- Feed requests/sec: 500M users × 15 requests/day ÷ 86,400s ≈ ~85K feed requests/sec; peak is 2–3× that, so ~200K feed QPS at peak.
- Candidates retrieved per request: ~500 candidates from retrieval, ranked down to ~10–20 shown.
- Latency budget: total p99 of ~200ms split roughly as: feature assembly ~30ms, retrieval (ANN lookup) ~50ms, ranking inference ~60ms, re-ranking + response assembly ~30ms.
- Training data volume: 200K feeds/sec × ~15 videos shown = ~3M impressions/sec; engagement labels arrive with up to ~30 min delay.
How the numbers affect the design
| Signal from the numbers | Design decision |
|---|---|
| 200K feed QPS, 200ms budget | Retrieval must be an in-memory ANN lookup in tens of milliseconds, not a scan over all videos. |
| 500M videos in catalog | Can't rank all videos per request — need the two-stage retrieval → ranking funnel. |
| 3M impressions/sec | Logging pipeline needs a high-throughput message queue (Kafka-scale). |
| Labels arrive 0–30 min late | Training data assembly must join impression logs with delayed engagement logs asynchronously. |
| 5M new videos/day | New video embeddings must be computed and indexed incrementally, not just in batch. |
| Features needed at serving | Online feature store must return features in single-digit milliseconds, not from a slow DB. |
Caution
Do not design a system that ranks all 500M videos per user request. Even scoring 500M candidates at 1ns each takes 500ms — far over budget. The two-stage funnel (retrieval to ~500 candidates, then rank those 500) is not an optimization; it is a hard requirement to hit the latency budget.
Core entities / data model
Six things are modeled: the users and videos (first-class objects), the engagement facts (source of truth for labels), the embeddings that power retrieval, the feature store entries that power ranking, and the impression logs that close the training loop.
| Entity | Key fields |
|---|---|
User |
user_id, demographics, account_age, device/locale metadata |
Video |
video_id, creator_id, upload timestamp, duration, language, tags, thumbnail |
UserEmbedding |
user_id → dense vector (e.g. 256-dim), updated periodically (hours) |
VideoEmbedding |
video_id → dense vector (256-dim), computed once at upload + refreshed as engagement grows |
EngagementEvent |
user_id, video_id, event_type (watch_pct, like, share, follow), timestamp |
ImpressionLog |
request_id, user_id, video_id, rank_position, features_snapshot, timestamp |
FeatureStoreEntry |
key: (user_id) or (video_id) or (user_id, video_id) → feature map, updated_at |
Embeddings are the retrieval currency. A two-tower model produces a user embedding and a video embedding such that a user's next likely videos have embeddings close to theirs in vector space. User embeddings are precomputed on a cadence (e.g. hourly) and cached; on session start a lightweight update can incorporate the most recent signals without a full tower recompute. Video embeddings are computed at upload and refreshed as engagement grows, living in an ANN index.
The impression log is the training data factory. Every video shown is logged with its features at serving time (not recomputed later — this is critical for preventing training/serving skew). Labels (watch time, likes, etc.) are joined in after they arrive — potentially minutes later.
Where each lives. User and video metadata in a sharded document store (partitioned by user_id / video_id). Embeddings in a vector database (e.g. Faiss-over-Milvus, Pinecone, or ScaNN). The online feature store in an in-memory KV store (Redis or Memcached) for sub-10ms reads. Impression logs and engagement events in a high-throughput event stream (Kafka) feeding an offline data warehouse (BigQuery / Hive).
API design
Three operations carry the system from the user's perspective: fetch the feed, record an impression batch, and record an engagement event.
Get personalized feed
GET /api/feed
Headers: Authorization: Bearer <token>
Query params: limit=20, session_id=...
200 OK
Returns: {
"videos": [
{ "video_id": "...", "cdn_url": "...", "creator": {...}, "rank_score": 0.92 },
...
],
"request_id": "req_abc123" ← used to correlate impression logs
}
The request_id is returned so the client can attach it to impression and engagement logs, enabling the offline join of what was shown vs what was engaged with.
Record impressions (batched)
POST /api/impressions
Body: {
"request_id": "req_abc123",
"impressions": [
{ "video_id": "...", "rank_position": 0, "shown_at_ms": 1720000000000 },
...
]
}
202 Accepted
Client calls this once per feed fetch, in the background. Returns 202 — the impression pipeline is async.
Record engagement event
POST /api/events
Body: {
"video_id": "...",
"request_id": "req_abc123",
"event_type": "watch_completion",
"value": 0.87, ← watch fraction (0–1) or 1 for like/share/follow
"timestamp_ms": 1720000042000
}
202 Accepted
Internal: retrieve candidates (called by feed service → retrieval service)
POST /internal/retrieve
Body: { "user_id": "...", "user_embedding": [...256 floats...], "limit": 500 }
Returns: { "candidates": [ { "video_id": "...", "retrieval_score": 0.83 }, ... ] }
Internal: score candidates (called by feed service → ranking service)
POST /internal/rank
Body: { "user_id": "...", "candidates": ["vid_1", ...], "context": { "hour_of_day": 14, ... } }
Returns: { "scores": { "vid_1": { "p_watch": 0.71, "p_like": 0.12, "p_share": 0.04, "combined": 0.61 }, ... } }
High-level architecture
Two distinct paths run in parallel: the serving path (user opens app → feed delivered in <200ms) and the training/data path (impressions + delayed labels → retrained models). A strong answer walks both.
1. Serving path — request flows through retrieval → ranking → serve
flowchart LR
Client["Client\n(mobile app)"] -->|"GET /feed"| FeedSvc["Feed Service"]
FeedSvc -->|"fetch user features\n+ user embedding"| FSOnline[("Online\nFeature Store")]
FeedSvc -->|"ANN lookup\nuser_emb → top-500"| RetrSvc["Retrieval Service\n(ANN Index)"]
RetrSvc -->|"500 candidate video_ids"| FeedSvc
FeedSvc -->|"fetch video features\nfor 500 candidates"| FSOnline
FeedSvc -->|"score 500 candidates"| RankSvc["Ranking Service\n(ML model)"]
RankSvc -->|"ranked scores"| FeedSvc
FeedSvc -->|"top-20 + re-rank\n(diversity, freshness)"| Client
FeedSvc -.->|"log impression\n(async)"| ImpQ[["Impression Queue\n(Kafka)"]]
- Feed Service reads the user's pre-computed embedding and real-time features (recent activity, session context) from the online feature store in one batch call.
- It fires an ANN lookup against the retrieval service's in-memory index, returning ~500 candidates in ~50ms. Multiple candidate sources (ANN results, trending, followed creators) are merged here.
- For those 500 candidates it fetches their pre-computed features from the online feature store.
- Ranking Service scores all 500 via the ranking model's inference endpoint, returning multi-task probability estimates per video.
- Feed Service combines the scores into a single value, applies light re-ranking rules (no two consecutive videos from the same creator, inject one "fresh" video, remove already-watched), and returns the top 20.
- Impression logging is fully async — it does not block the response.
2. Training / data path — impressions + delayed labels → model updates
flowchart LR
Prod["Feed Serving\n(logs impressions)"] -->|"impression events"| ImpQ[["Kafka:\nImpressions"]]
Client2["Client\n(sends events)"] -->|"watch/like/share events"| EngQ[["Kafka:\nEngagement"]]
ImpQ --> Joiner["Label Joiner\n(streaming join,\nwait up to 30min)"]
EngQ --> Joiner
Joiner -->|"(impression, label) pairs"| DW[("Data Warehouse\n(BigQuery / Hive)")]
DW -->|"training dataset"| TrainPipe["Training Pipeline\n(two-tower +\nranking model)"]
TrainPipe -->|"new model artifacts"| ModelReg[("Model Registry")]
ModelReg -->|"push updated model"| RetrSvc["Retrieval Service\n(refresh ANN index)"]
ModelReg -->|"push updated model"| RankSvc["Ranking Service\n(hot-reload weights)"]
DW -->|"precompute features"| FSOffline[("Offline\nFeature Store")]
FSOffline -->|"sync fresh values"| FSOnline[("Online\nFeature Store")]
The label joiner is the critical piece: it holds impression records in a buffer window (up to 30 min) waiting for engagement events to arrive and attaches them, then writes complete (impression, label) rows to the data warehouse. This is where delayed labels are handled — the joiner accepts late arrivals and can emit partial updates.
3. Combined — the full picture
flowchart LR
User["User"] --> App["Mobile Client"]
App -->|"GET /feed"| FeedSvc["Feed Service"]
FeedSvc --> FS[("Online Feature Store")]
FeedSvc --> ANN["Retrieval / ANN"]
FeedSvc --> Rank["Ranking Service"]
FeedSvc --> App
App -.->|"impressions + events\n(async)"| Kafka[["Kafka"]]
Kafka --> Join["Label Joiner"]
Join --> DW[("Data Warehouse")]
DW --> Train["Training Jobs"]
Train --> Models[("Model Registry")]
Models --> ANN
Models --> Rank
DW --> FSOffline[("Offline\nFeature Store")]
FSOffline --> FS
The serving path is synchronous and latency-critical; the training path is asynchronous and throughput-critical. They share the feature store as a bridge: offline jobs precompute features into the store, which serving reads at low latency.
Deep dives
1. Retrieval — two-tower model, ANN index, and candidate blending
The retrieval stage must reduce 500M+ videos to ~500 candidates in ~50ms. The bottleneck is that you cannot run a joint model over all 500M candidates per request — the only viable approach is to precompute embeddings for users and videos separately, then do a fast nearest-neighbor lookup.
The two-tower architecture.
flowchart LR
UserFeats["User features\n(history, demographics,\nreal-time context)"] --> UT["User Tower\n(MLP → 256-dim)"]
VideoFeats["Video features\n(tags, transcript,\naudio, creator, engagement)"] --> VT["Video Tower\n(MLP → 256-dim)"]
UT -->|"user embedding u"| Dot["dot(u, v)\n→ relevance score"]
VT -->|"video embedding v"| Dot
Dot -->|"trained on\nwatch/engagement labels"| Loss["In-batch softmax + logQ correction\n[training only]"]
Both towers produce dense vectors in the same 256-dimensional space. The model is trained so that a user vector is close to vectors of videos the user would engage with. Crucially, the two towers are separate at inference time: the video embeddings are precomputed once (refreshed daily or on new engagement), and the user embedding is precomputed on a cadence (e.g. hourly) and cached in the feature store; a lightweight session-start update can incorporate recent signals without a full tower recompute. Full per-request tower recomputation is the exception (e.g. real-time serving experiments), not the default. This separation is what makes ANN lookup feasible.
Why not a single joint model for retrieval? A joint model takes (user, video) as input and produces a score. To rank 500M videos you'd need 500M forward passes — impossible at serving time. The two-tower design allows computing video embeddings offline and caching them; only the user embedding is computed online, and finding the top-k nearest video vectors is a fast index lookup.
The ANN index. Video embeddings are inserted into an approximate nearest neighbor index — HNSW (Hierarchical Navigable Small World graphs, used in Faiss and Weaviate) or ScaNN. At query time, the user embedding is the query vector and the index returns the k nearest video vectors in ~10–50ms, even over 500M entries, trading a small accuracy loss (a few percent of the true top-k missed) for orders-of-magnitude speedup over exhaustive search.
Exhaustive exact search — correct but impossibly slow at scale
Score the user embedding against all 500M video embeddings exactly, take the top 500. With a dot-product taking ~10ns per vector, 500M comparisons take ~5 seconds. Far over budget. Beyond the FLOP count, exhaustive search is memory-bandwidth-bound: 512GB of embeddings cannot fit in any reasonable cache, so most accesses hit DRAM or disk, making real-world latency far worse than the naïve FLOP estimate implies.
- Pro: returns the exact top-500 with no missed items.
- Con: O(N) per query, ~5s for N=500M. Does not scale.
- Verdict: only viable for a catalog of tens of thousands, not hundreds of millions. Mention it to show you understand the tradeoff, then dismiss it.
ANN index (HNSW / ScaNN) — recommended at scale
An ANN index organizes vectors so a query can find the approximate top-k by traversing a graph or quantized codebook, visiting only a small fraction of the 500M candidates. HNSW builds a layered proximity graph; ScaNN uses quantization + reordering. Both return top-500 in ~10–50ms for 500M vectors.
The key engineering decisions: how often to rebuild or incrementally update the index (new videos must appear in the feed within hours, so the index needs incremental inserts, not just nightly rebuilds); how much RAM the index needs (500M × 256-dim × 4 bytes ≈ 512GB for raw vectors — but HNSW graph metadata adds ~2–4× overhead, bringing the total to 1–2TB; using product quantization / ScaNN compresses each vector to ~8–16 bytes, cutting memory ~30–60×); and how to shard (by video_id range, or random for load balance).
- Pro: sub-50ms retrieval over hundreds of millions of vectors, horizontally scalable.
- Con: returns approximate results — some relevant videos are missed. Accuracy tuned via the index's probe/beam parameters (more probes = higher recall, higher latency). The small miss rate is acceptable because the ranking stage reorders the retrieved set anyway.
- Verdict: the only viable approach at this scale. Use HNSW for ease of incremental inserts; use ScaNN for better throughput at very large scale.
Candidate blending. The two-tower ANN results are one source. A strong answer blends multiple candidate sources before ranking:
| Source | Why include it | Approximate count |
|---|---|---|
| Two-tower ANN (personalized) | Personalized to user taste | ~300 |
| Recent/trending (last 24h) | Prevents staleness; injects viral content | ~100 |
| Followed-creator videos | Social graph signal; high-intent audience | ~50 |
| Diversity buckets | Ensure topic/creator variety | ~50 |
Total: ~500 candidates, passed to the ranking stage. The blending weights are tunable; trending can be suppressed for users with strong taste signals.
Tip
A SWE interviewer stops here — they want to see the funnel (retrieval → ranking), how the ANN index is served, and how candidate sources are blended. An MLE interviewer will push on the two-tower training setup: negative sampling and logQ correction, how embeddings stay fresh, and whether you handle position and exposure bias in the training data.
Similarity metric and normalization. If embeddings are L2-normalized (unit vectors), cosine similarity and inner product are equivalent — either works in training and the ANN index. If embeddings are not normalized, use inner product consistently: in the training loss, the ANN index metric, and retrieval scoring. Never mix cosine and inner-product across these three places; mismatched metrics silently degrade retrieval quality.
Negative sampling. The system trains with in-batch negatives (shown in the diagram): each item in a training batch serves as a negative for every other item's user. In-batch negatives over-represent popular items as negatives — popular items appear in batches more often, introducing a popularity bias. The standard fix is a logQ correction: subtract the log sampling probability log Q(v) from each logit in the softmax, which de-biases the loss toward the true engagement distribution. Sampled softmax — drawing negatives explicitly from the item-frequency distribution — is an alternative that achieves a similar correction differently; the two are not synonyms. Hard negatives (items retrieved by the current model but not engaged with) further improve discriminability.
2. Ranking — features, multi-task objectives, and the feature store
The ranking stage takes ~500 candidates and scores each with a heavier model, returning the order the user will see. This is where the most ML depth lives.
The ranking model. The model takes a (user, video, context) feature vector and predicts several engagement probabilities simultaneously — this is a multi-task model with one shared body and multiple prediction heads:
p(watch_completion)— predicted fraction of video watchedp(like)— predicted probability of a likep(share)— predicted probability of a sharep(follow_creator)— predicted probability of following the creator
The final ranking score combines these:
score = w1 * p(watch_completion) + w2 * p(like) + w3 * p(share) + w4 * p(follow_creator)
The weights w1..w4 are tuned to reflect business objectives (watch time valued most, follows valued highly because they signal strong intent). Multi-task is preferred over separate models because the tasks share signal — a user who tends to like also tends to share, and joint training generalizes better than training four independent models.
Features. Three feature groups feed the ranking model:
| Group | Examples | Freshness needed |
|---|---|---|
| User features | age of account, device, long-term taste embeddings, genre preferences | Hours |
| Video features | view count, like rate, share rate, creator follower count, video age, content tags, language | Minutes |
| Cross features | user's past engagement with this creator, user's past engagement with this content category, user's watch history recency | Minutes–hours |
The feature store and training/serving skew. The most important operational concern in ranking is that the features used during training must exactly match the features available at serving time. If the training pipeline uses "video like rate as of training time" but serving uses a slightly different computation (different window, different normalization), the model performs worse in production than offline metrics predict. This mismatch is training/serving skew, and it is one of the most common silent killers of ML ranking quality.
The fix is a unified feature store: the same feature computation logic runs both when training data is assembled (offline) and when features are fetched for a live request (online). The offline path writes precomputed features to a data warehouse; the online path reads from a low-latency in-memory cache (Redis), which is a mirror of the same values. The impression log must snapshot the feature values at serving time, so when the label arrives later and training data is assembled, the model trains on the same feature values that were actually used — not on recomputed values that might differ.
flowchart LR
Compute["Feature\nComputation Logic\n(shared code)"] -->|"offline batch"| Offline[("Offline Store\n(BigQuery)")]
Compute -->|"stream / incremental"| Online[("Online Store\n(Redis)")]
Offline -->|"training features"| TrainJob["Training Job"]
Online -->|"serving features\n<10ms"| FeedSvc["Feed Service"]
FeedSvc -->|"snapshot features\nin impression log"| ImpLog["Impression Log"]
ImpLog --> TrainJob
Caution
Logging features at training time (by recomputing them from raw data) instead of snapshotting them at serving time is the most common cause of training/serving skew. A video's like rate at the moment it was served may be very different from its like rate 30 minutes later when the training job runs. Always log the feature values that were actually used to make the serving decision.
Calibration. Two separate concerns are often conflated here. (a) Calibration for probabilities: the model's raw outputs should match observed rates so that "p=0.7" actually means the event happens 70% of the time — this is needed for meaningful business KPIs, showing "90% match" in a UI, or any downstream system that interprets probabilities literally. (b) Calibration for combining scores in a ranking: a weighted multi-task ranking score (w1 * p_watch + w2 * p_like + ...) can work with uncalibrated outputs if the weights are tuned on validation or engagement data — this is what production ranking systems typically do, and it does not require the outputs to be true probabilities. Calibration additionally puts each head on a true-probability footing, but comparable influence in the final score comes from the tuned weights rather than from calibration alone (a watch-completion rate of 0.8 and a share rate of 0.01 have very different natural ranges regardless of calibration); it is not strictly required to combine scores for ranking purposes. The model's raw output probabilities are calibrated via isotonic regression or Platt scaling on held-out data, primarily for KPI reporting and comparable-scale combination.
Light re-ranking. After the model scores all 500 candidates, a rule-based re-ranking pass applies:
- Diversity: no more than N consecutive videos from the same creator; inject topic variety.
- Freshness injection: guarantee at least one video uploaded in the last few hours.
- Business rules: suppress explicitly disliked creators; apply content policies.
- Exploration slot: with probability ~5%, insert a random video from outside the top scores (see Deep Dive 3).
Tip
SWE interviewers care about the feature store's online/offline architecture and the logging pipeline. MLE interviewers care about the multi-task objectives, calibration, and why you need to snapshot features at serving time rather than recomputing them for training.
3. Cold start, delayed labels, feedback loops, and evaluation
These are the three "production realities" that separate a textbook design from one that actually works. A SWE interviewer will ask about cold start; an MLE interviewer will ask about all three plus evaluation.
Cold start — new users.
A new user has no watch history, no engagement signal, no personal embedding. Three strategies, applied in combination:
- Onboarding interest selection. At sign-up, ask the user to select 3–5 interest categories. Use those to retrieve content from popular videos in those categories, bypassing the personalized two-tower model entirely.
- Popularity / trending fallback. Show high-engagement recent videos as the default before personal signals accumulate. This ensures the user sees content that "most users enjoy" rather than a blank feed.
- Rapid cold-start learning. After the first ~10 interactions (watches, skips, likes), a lightweight model can already infer rough preferences. Update the user embedding from these early signals (even with a simple average of watched video embeddings) and begin personalizing faster than waiting for a full daily retrain.
Cold start — new videos.
A new video has no engagement history and no embedding trained on engagement. Two strategies:
- Content-based embedding. Use the video's content features (audio features, visual features from frames, text from auto-generated transcript, hashtags) to produce an initial embedding via a content encoder, bypassing the two-tower engagement signal entirely. This allows the video to appear in retrieval before it has any watch data.
- Forced exploration. Deliberately show new videos to a small fraction of users regardless of their predicted score (the "exploration slot" from re-ranking). This generates the first engagement labels the model needs to learn from.
Delayed labels.
A user watches a video and the impression is logged instantly. But watch completion is measured only after they finish (or swipe away) — which could be up to 3 minutes for a long video. Likes and shares can arrive up to 30 minutes after the impression (users sometimes like a video after watching several more). This means the training label for a given impression is not available at impression time.
The label joiner handles this: it keeps impression records in a buffer (e.g. a Kafka topic with a retention window), waits up to a configured delay (say 30–60 minutes), and joins arriving engagement events to their impression. Late events update the label; impressions with no engagement after the window become implicit negatives — but note that "implicit negative" means different things per task: for binary tasks (like, share), a no-engagement impression is a genuine negative label 0; for watch-completion (a regression target), a no-engagement impression is typically labeled 0 or excluded, since the two tasks have different semantics. Don't treat all implicit negatives identically across task types. The joined records are written to the training dataset.
Caution
Do not train on impression records immediately at serving time without joining engagement labels. "No engagement seen yet" is not the same as "no engagement." Training before the label window closes treats unlabeled positives as negatives, producing a biased dataset that penalizes content that generates delayed engagement (e.g. videos people like after reflection).
Train immediately on each impression — fast pipeline, biased labels (avoid)
Write impression records to the training dataset as soon as they are logged, before the label window closes. Labels that haven't arrived yet are treated as "no engagement."
- Pro: minimal pipeline complexity; low label latency.
- Con: impressions with delayed positive engagement are mislabeled as negatives. The model learns to penalize content that earns engagement with a delay — exactly the content most underserved by existing rankings. The bias compounds over retraining cycles.
- Verdict: do not do this. "No label yet" is not "no engagement."
Fixed short window join — better, but misses late arrivals
Wait a fixed short window (e.g. 5 minutes) after each impression before writing the training record. Engagement events that arrive within the window are attached; later events are discarded.
- Pro: simple to implement; catches most fast engagements (watches, immediate likes).
- Con: late engagements (shares and likes that arrive 30+ minutes after watching) are missed. The training dataset under-counts delayed positive signals, introducing a milder version of the same bias.
- Verdict: acceptable as a first pass. Choose the window based on engagement latency distributions for each signal type; don't use a single window for all tasks.
Buffered label-joiner with configurable wait window — recommended
A streaming label-joiner (e.g. Flink or a Kafka Streams job) keeps impression records in a time-bounded buffer, waits up to a configurable window (e.g. 30–60 minutes per signal type), and joins arriving engagement events before writing the final training record. Late-arriving events update the label; impressions with no engagement after the window are written as implicit negatives.
- Pro: captures the majority of delayed signals; configurable per signal type (shorter for watch-completion, longer for likes/shares); decouples label latency from model training latency.
- Con: more pipeline complexity; adds label latency before training data is available; requires handling out-of-order events.
- Verdict: the right design at production scale. Tune the wait window per signal using historical engagement-arrival latency distributions.
Feedback loops, exposure bias, and position bias.
Two distinct biases arise from how training data is collected, and it is worth naming them separately:
- Exposure bias (the feedback loop): the model only observes outcomes for videos it chose to show. Unshown videos receive no engagement signal, so the model becomes progressively blind to content it never surfaces — concentrating creator visibility and hurting discovery over time.
- Position bias: videos shown at higher rank positions receive more engagement simply because they are more visible, not because they are better. If training data is collected naively, the model learns to score already-top-ranked items higher, compounding the ranking of popular positions. The standard mitigations are (a) including rank position as a training feature so the model can disentangle position effects from item quality, or (b) inverse propensity score (IPS) weighting — downweighting high-position impressions by their propensity to receive clicks.
The standard mitigations:
- Epsilon-greedy exploration: with probability ε (~5%), replace one feed slot with a randomly selected candidate outside the top scores. This generates unbiased signal on items the model would not otherwise explore.
- Thompson sampling / UCB: treat ranking as a multi-armed bandit; prefer videos with high uncertainty (wide confidence interval on predicted engagement) to gather information faster. More complex but more efficient than pure random exploration.
- Counterfactual logging: log the scores and features of items that were not shown alongside items that were. This enables offline evaluation of what-if scenarios and is required for unbiased offline metrics.
Offline and online evaluation.
Offline metrics (evaluated before deploying the model): AUC, NDCG (for ranked lists), mean average precision. But offline metrics are imperfect because they measure the model's ranking on previously shown impressions — they do not capture feedback loop effects or how users would respond to a different distribution.
Online metrics (evaluated via A/B tests): total watch time per session, likes and shares per session, next-day retention, long-term user satisfaction. These are the ground truth. A model that improves offline AUC but hurts next-day retention is not a model to ship. Run A/B tests with a 1–2 week holdout before full rollout; monitor for novelty effects (users explore more when something changes) decaying over time.
Warning
Offline AUC can look great while the model produces a worse feed in production. The feedback loop means offline evaluation is always measured on a biased, historically-shown dataset. Never skip the A/B test; it is the only honest evaluation signal.
Tradeoffs & bottlenecks
- Two-stage funnel: recall vs precision. The retrieval stage sets the ceiling on ranking quality — if a relevant video is not retrieved, the ranker never sees it. Increasing retrieval recall (e.g. 500 → 1000 candidates) gives the ranker more to work with but costs more feature fetches and ranking compute. The right retrieval size balances latency budget against recall.
- Embedding freshness vs cost. User embeddings updated every few hours may miss recent session context; updating every request is expensive. A pragmatic split: batch-update user embeddings every few hours, supplement with a small real-time feature (last N watched videos, current session context) that the ranking model can use without recomputing the full embedding.
- Multi-task vs single-objective. Optimizing only watch time produces videos users watch passively but don't engage with; optimizing only likes produces click-bait. Multi-task learning jointly optimizing several objectives is more expensive to train and tune but produces better overall user satisfaction. Choosing the task weights is a business decision, not a purely technical one. A risk in multi-task learning is negative transfer: when objectives conflict (e.g. watch-completion and likes are weakly correlated), a fully shared network can underfit both. Architectures like MMoE (Multi-gate Mixture-of-Experts) and PLE (Progressive Layered Extraction) address this by using task-specific sub-networks (experts) over a shared base, letting each task draw on different expert combinations and reducing interference.
- Training/serving skew is silent. A mismatch between training features and serving features does not produce an error — it produces a model that works in staging but underperforms in production. Strong teams run automatic skew detection: compare the distribution of each feature in serving logs against training data.
- ANN index update latency. Incremental inserts into HNSW are possible but slower than batch rebuilds. New videos appear in the ANN index within minutes via incremental insert; a full nightly rebuild optimizes graph quality. Running both (incremental for freshness, periodic full rebuild for quality) is standard.
- Feedback loops compound over time. The more mature the system, the stronger the feedback loop effect. Exploration must be a permanent, first-class design decision — not a short-term fix — or the feed gradually narrows to a self-reinforcing set of creators and topics.
Extensions if asked
If the interviewer wants to push beyond the core system:
- Creator fairness and diversity. Beyond user satisfaction, optimize for creator exposure diversity — prevent the feed from concentrating on a small set of dominant creators. Requires fairness constraints in re-ranking or as an optimization objective.
- Real-time event streaming to the model. Instead of hourly feature refreshes, stream user events (watches, likes) into a real-time feature pipeline (Flink/Spark Streaming) that updates the online feature store within seconds. Needed for sessions where user preference shifts rapidly.
- Contextual bandit training. Instead of pure supervised learning from logged labels, train the ranking model as a contextual bandit with an inverse propensity scoring (IPS) correction to debias the feedback loop more aggressively.
- Cross-surface ranking. The same model serves the main feed, the search results page, and the notifications tray. Designing a shared ranking service that adjusts for surface-specific business rules (search wants precision; the main feed wants engagement; notifications want relevance without overload) is a real production architecture problem.
- Embedding model compression. 256-dim embeddings across 500M videos require large ANN shards. Product quantization (PQ) compresses embeddings to ~8 bytes while retaining >95% recall, dramatically reducing the index memory footprint and enabling in-memory serving on a smaller fleet.
What interviewers look for & common mistakes
What interviewers usually reward:
- Naming the two-stage funnel immediately. "We can't rank 500M videos per request — we need retrieval to bring it to ~500 candidates, then rank those." This one sentence shows you understand the latency constraint and the purpose of the architecture.
- Explaining the two-tower model correctly. Separate user and item encoders, precomputed video embeddings, ANN lookup, trained on engagement labels. A SWE can describe it at the component level; an MLE should know the training loss and negative sampling.
- The feature store and training/serving skew. The unified feature store that serves both online and offline, and logging feature values at serving time (not recomputing them for training) — this is the detail that separates candidates who've built these systems from those who've only read about them.
- Delayed labels and the label joiner. Acknowledging that engagement signals arrive after the impression and explaining how the training pipeline handles the join window. Ignoring this produces silently biased training data.
- Feedback loops + exploration. Naming the exposure bias problem and proposing a concrete mitigation (epsilon-greedy slot, exploration budget) — not just "we'll add diversity" without a mechanism.
- Multi-task ranking objectives. Explaining why optimizing only watch time is insufficient and how multi-task scoring + calibration combines objectives.
- Concrete latency budget. Allocating the ~200ms across retrieval (~50ms), feature fetch (~30ms), ranking (~60ms), and re-ranking (~30ms). Shows you've thought about the serving path, not just the model.
Before you finish, do a quick mistake check:
- Did you start with the two-stage funnel, not "run the ranking model over all videos"?
- Did you describe the two-tower model as two separate encoders with precomputed video embeddings and ANN lookup — not a joint model run at query time?
- Did you discuss the feature store and the risk of training/serving skew?
- Did you address delayed labels — not treating "no label yet" as "no engagement"?
- Did you propose a concrete mechanism for breaking the feedback loop (exploration), not just mention diversity in passing?
- Did you name at least two cold-start strategies for new users and new videos?
- Did you explain that A/B tests (not offline AUC alone) are the true evaluation signal?
Practice this live
Run this exact question with our AI voice interviewer and get feedback.
Start the interview →