SysDesignPrep.com
Interviewer kit

Design TikTok

Run this for someone else. You hold the answers; they do not. Read the prompt, keep the clock, and use the probes below when an answer is thin. Do not show them this page.

The candidate should have a blank page and a whiteboard, not this.

Open with this

A full-screen feed of short videos for 1 B daily users, where the next clip starts instantly and every swipe reshapes what comes next. Take a couple of minutes on requirements, then we will do some numbers, then the design. I will interrupt to keep us moving.

The clock

  • 4 min: functional requirements and scope
  • 4 min: non-functional requirements, with numbers
  • 5 min: back-of-envelope estimates
  • 16 min: high-level design and one or two flows
  • 16 min: deep dives and the close

Move them on out loud when a section overruns. The commonest failure is spending twenty minutes on requirements and never reaching a deep dive, and preventing that is your job as much as theirs.

Requirements · 8 min

Listen for: a scoped set of capabilities, an explicit out-of-scope list, and numeric targets rather than adjectives. Prompt with “what are you not building?” if they never scope, and “what number would make that requirement real?” if they say “fast” or “highly available”.

Functional (10)
  • Post a short video: 15 s to 3 min, from a phone, with a caption, a sound and hashtags. Watchable within a minute of the upload finishing, at full quality a few minutes later.
  • An endless, full-screen For You feed: One video at a time; a swipe starts the next one instantly, with no spinner, on Wi-Fi or on a weak cellular link.
  • The feed adapts within the session: Completions, rewatches, quick skips, likes and shares change the next page of the feed, not tomorrow’s.
  • Followed accounts mixed in: New uploads from accounts you follow appear in For You, at most one per page, as well as in a separate Following tab.
  • No repeats: A video already watched does not come back for about four weeks, and never within the same session.
  • Likes, comments, shares and follows: Each like counts exactly once per user. Like and comment counts show on every video.
  • View counts: Shown rounded and approximate, refreshed within seconds; exact numbers for creator analytics come later from batch.
  • A fair start for new videos and new users: A video from an account with no followers can reach a large audience if its viewers respond to it. A new user’s feed is personal within minutes.
  • Safety before reach: Automated checks before the first view, human review before a video reaches a wide audience, and a removal stops distribution within minutes.
  • Out of scope: Live streaming (see Design Twitch), direct messages, search, ads, creation tools and effects, music licensing and creator payouts. Long-video transcoding and ABR (Design YouTube) and ranking models (Recommendation System) only where short video changes them.
Non-functional (8)
  • Scale (1 B DAU · 2 M starts/s peak): 50 B video starts and 30 M uploads a day. Every start is megabytes, so bandwidth, not request handling, is the bill.
  • Swipe to first frame (p95 < 150 ms · < 1 % of swipes stall): This drives the hardest trade-off in the design: prefetching deeper makes more starts instant, but wastes more bytes on metered data and makes the feed slower to react to what the user just did.
  • Data per start (~2.5 MB · prefetch waste < 10 %): Users on metered plans notice an app that eats data. Every byte fetched and never played is also egress paid for.
  • Upload to watchable (p95 < 60 s after upload): Creators post and then watch the counter. One rendition and the automated checks are enough to go public; the full ladder follows.
  • In-session freshness (every play shapes the next page): The feed is only as fast as its slowest loop that the user can feel. Outcomes ride on the page request, so the next page never waits for a stream.
  • Feed page latency (p99 < 200 ms server side): Only the first page of a session is on the critical path; later pages are requested four videos early.
  • Safety (checked before view 1 · reviewed before 20 k): A harmful video seen by 200 people is a takedown; seen by millions it is an incident. Reach grows in steps so that review comes before the big ones.
  • Availability (99.95 % of opens play within 2 s): Degrade instead of failing: a cached, slightly stale feed or a popular one beats an error on the app’s only screen.

Estimates · 5 min

Ask for two or three numbers, not all of them. What matters is whether they state assumptions, round sensibly, and say what the number implies. Push once with “where did that come from?”

The numbers (8)
  • Video starts per second: ~580 k/s avg · 2 M/s peak. 1 B DAU × 50 swipes = 50 B starts a day ÷ 86 400 ≈ 580 k/s. The evening peak is 2 M/s, about 3.5× the average. Each start is a view, a watch event and, usually, a few range requests for the rest of the clip.
  • Feed pages per second: ~72 k/s avg · 250 k/s peak. Pages hold 8 videos: 2 M starts/s ÷ 8 = 250 k pages/s at peak; 50 B ÷ 8 = 6.25 B pages a day, ~72 k/s on average. Every page is filtered and ranked, so small pages cost more server work per video than a 50-video page would. That is the price of a feed that reacts.
  • Bytes per start and peak egress: ~2.5 MB · ~40 Tbit/s. Average delivered bitrate ~1 Mbit/s (125 KB/s). A start fetches ~20 s of video: ~15 s watched plus ~5 s prefetched or buffered and never played. 20 × 125 KB = 2.5 MB. At peak 2 M/s × 2.5 MB = 5 TB/s ≈ 40 Tbit/s, ~125 PB a day. This is the single biggest cost in the system.
  • Prefetch per swipe: ~250 KB · waste ~6 %. Each swipe moves the prefetch window by one, so one new head enters it: the first 2 s at ~1 Mbit/s ≈ 250 KB (200 KB at 540p on cellular, 375 KB at 720p on Wi-Fi). The waste is the heads still queued when the app closes: 4 × 250 KB ≈ 1 MB per session × 8 sessions × 1 B users ≈ 8 PB a day, ~6 % of egress.
  • Uploads and the fast lane: ~350/s avg · ~1 k/s peak. 30 M ÷ 86 400 ≈ 350 uploads/s, ~1 k/s at peak. At ~15 MB per 30-s clip (encoded on the phone at ~4 Mbit/s) that is ~450 TB a day of originals. The fast 540p encode is ~5 core-seconds a clip, so even the peak needs only ~5 k cores; the rest of the ladder (~30 core-seconds) runs later on ~10 k.
  • Rendition storage per day: ~300 TB/day. A 30-s clip at 360p (0.4 Mbit/s), 540p (0.8) and 720p (1.5) is 1.5 + 3 + 5.6 ≈ 10 MB. 30 M × 10 MB = 300 TB/day, ~110 PB a year. 1080p and HEVC copies are made only for the ~10 % of videos that pass the pool’s first stage, adding ~20 %. Originals go straight to an archive tier.
  • Seen-set memory: ~2 TB · ~6 TB replicated. 50 plays a day × 28 days = 1 400 videos per user. A Bloom filter at 1 % false positives needs ~10 bits per entry: ~1.75 KB per user. 1 B users × 1.75 KB ≈ 1.75 TB, ~6 TB with three replicas: small enough to keep in memory beside the session state.
  • Traffic-pool impressions: ~13 B/day · ~1 in 4 starts. 25 M eligible uploads × 200 test views = 5 B. The top 10 % get 2 000 (5 B), the top 5 % of those 20 000 (2.5 B), the top 2 % of those 200 000 (0.5 B): ~13 B of 50 B starts, about 2 slots in every page of 8. Only the first stage is a real cost; later stages show videos that have already proven themselves.

High-level design · 16 min

Let them draw. Interrupt only to ask what backs a component or what a box actually does. Then pick one flow below and ask them to walk it end to end.

Components (14)
  • Edge PoPs (CDN + upload ingest): Hundreds of PoPs close to users, with regional shields behind them. They serve video bytes (heads of popular videos kept at the edge, bodies by range from the shield on a miss) and terminate uploads near the creator, relaying chunks to the regional store over the provider’s backbone. A short round trip is what makes both a 250 KB head and a 15 MB upload fast on a lossy cellular link.
  • Video store (object storage · head + body): Originals (archive tier once encoded) and, per rendition, two immutable objects under a versioned path: a head (init segment plus the first 2 s, ~250 KB) and a body (the rest, in 2-s fragments read by byte range). Splitting the head out lets the CDN keep the most-requested bytes of every popular video at the edge while bodies sit at the shields. It is the CDN’s origin.
  • Media pipeline (fast lane · ladder · auto checks): Triggered when an original lands. The fast lane decodes once, encodes a 540p rendition in about 2 s on 4 cores and, beside it, runs the automated checks: perceptual hashes against known illegal content, classifiers on a frame a second, speech and caption text, and an audio fingerprint. The slow lane adds 360p and 720p within minutes, and 1080p and HEVC only once the pool promotes a video.
  • Video service (sharded SQL · by video id): Source of truth for each video: creator, caption, sound, status (public, restricted, removed), renditions with their real bitrates, plus a per-creator index of recent uploads for the following source. It makes a video public only once the fast lane and the automated checks have both reported, then admits it to the traffic pool. Feed hosts cache its serving records for at most 30 s, so a removal stops new serves within half a minute.
  • Mobile app (player · queue · heads): Plays one full-screen video and keeps a queue of the next ones, with the first 2 s (the head) of the next 3 to 5 already downloaded and the next player decoding its first frame, so a swipe shows a picture at once. Asks for the next page of 8 when 4 unplayed videos remain, sending how each recent play went. Uploads in resumable chunks to the nearest edge, starting while the creator is still writing the caption.
  • Feed service (per page · ~100 ms): Builds every page of 8, behind the usual API gateway (not drawn). Reads the session, seen filters and features in one round trip, folds in the outcomes the app sent, refreshes candidates when the session has moved, drops anything seen or not public, has ~200 scored, and fills slots for the pool and followed accounts. Then it picks one rendition per video for the app’s network class and signs the URLs. Stateless: the session lives in the online store.
  • Model serving (ANN retrieval · GPU ranker): The slow-moving half of recommendation: an ANN index of recommendable videos queried with long-term and session vectors, and a multi-task ranker on GPUs with completion, rewatch, skip, like, share and follow heads. At 250 k pages/s × 200 candidates it scores 50 M rows a second, ~500 GPUs at peak. Retrained and rebuilt daily; within a session only its inputs change, never its weights.
  • Moderation (human review queue): Human review for the pool’s winners before they reach 20,000 viewers, about 125 k videos a day ordered by how fast each is climbing, plus user reports and appeals. Verdicts go to the video service: approve, restrict (followers only, never recommended) or remove. The automated checks ran earlier, in the pipeline; this is where people look at what is about to be seen widely.
  • Event stream (Kafka · key=user_id): One play event per video started (watch ms, length, loops, skipped within 2 s, rendition, stalls), plus likes and shares, batched by the app: ~2 M events a second at peak in ~500 k requests. Keyed by user so per-user state is updated in order; per-video aggregates are re-keyed and pre-aggregated downstream, so a viral video is never one hot partition.
  • Engagement service (likes · follows · comments): Likes, comments, shares and follows, each a row written once per user, keyed by (video, user) or (follower, followee), with like and comment counts kept as sharded counters beside the rows. Serves a user’s followees to the feed at session start, and like counts and “liked by me” for the 8 videos of each page. A like is exact and idempotent; the count shown is rounded and may lag a second.
  • Online store (KV · session · seen · stats): In-memory KV with three key families: per-session state (session vector, ids served this session, ~1,500 cached candidates, recent outcomes), per-user seen filters (four weekly Bloom filters, ~1.75 KB) and features, and per-video stats including the approximate view count. A user’s keys share a hash tag, so the user half of a page is one round trip. Losing a session costs a slightly less personal next page, never an error.
  • Traffic pool (stages 200 → 200 k): Every new public video starts here with a test audience of 200, drawn from users whose interests sit near its content and, first, in its own region and language. When a stage’s views are in, it compares completion, rewatch, share and skip rates with videos of the same length and topic and promotes the top share to an audience ten times larger: 2,000, 20,000, then 200,000. Nothing passes 2,000 views without human review.
  • Training (daily · offline): The slow loop. Joins a day of plays with the features the feed served, retrains the ranker and the two-tower embeddings, rebuilds the ANN index with yesterday’s pool graduates in it, and ships new versions to model serving after offline checks and a canary. Minute-fresh signals never wait for it: they reach the feed through the session and the stream.
  • Stream processor (Flink · seconds): Turns plays into state within seconds: adds played videos to the user’s current seen filter, updates recent history and topic affinities, aggregates per-video stats (views, completion, rewatch, quick skips) every 5 s into the online store, and feeds each pool video’s stage counters. It also lands every event in the warehouse that training reads.
Flows to ask them to walk (5)
  1. Swipe: the next video starts instantly: The main read path. Pages of 8 arrive a few videos before they are needed, and the first seconds of the next videos are already on the phone, so a swipe never waits for the network.
    1. The app asks for the next page when 4 unplayed videos remain, with its network class and how each recent play went.
    2. The feed service reads the session, the seen filters and the user’s features in one round trip.
    3. If the session has moved, it refreshes candidates, and it asks the pool for new videos meant for this user.
    4. Candidates already seen, already served this session or no longer public are dropped.
    5. The survivors are scored with the session’s fresh features, and the page of 8 is assembled.
    6. For each video it picks one rendition for the network class and signs the URLs.
    7. The app prefetches the heads of the next videos, as deep as its network class allows.
    8. On a swipe the first frame is already decoded, and the app fetches the body a few seconds ahead.
    9. An edge miss on a less popular video is filled from the regional shield, then the store.
  2. Post a video, watchable within a minute: The core write. The upload starts before the creator taps Post, one quick rendition and the automated checks run side by side, and the video is public before the full ladder exists.
    1. While the creator writes the caption, the app uploads the clip in chunks to the nearest edge.
    2. On Post, the edge writes the original and the post’s details to the regional store, then acknowledges.
    3. The pipeline’s fast lane encodes one 540p rendition while the automated checks run beside it.
    4. When both have reported, the pipeline writes the video record with its first rendition and the checks’ result.
    5. The video service admits the new public video to the traffic pool at stage 0.
    6. Within minutes the slow lane adds 360p and 720p, and the video service lists them.
  3. A new video climbs the pool and goes viral: Cold start and the scale-breaking case. A video from an account with no followers earns each larger audience with its own numbers, passes human review before 20,000 people see it, then has to survive 50,000 starts a second.
    1. Feeds of users close to the video’s content give it their test slot until it has 200 views.
    2. Each play’s outcome reaches the video’s stage counters within seconds.
    3. With its views in, the pool compares it with videos of the same length and topic and promotes the top 10 %.
    4. Promoted again past 2,000 views, it waits for human review before going to 20,000.
    5. The reviewer approves it; the video service tells the pool, which opens stage 2.
    6. It graduates and takes off: 50,000 starts a second, served from edge memory.
    7. Views are aggregated per 5 s before they are written; likes are rows with sharded counts.
  4. Weak signal, retries and a degraded feed: The failure path. The queue on the phone is the buffer: a clip steps down one rendition rather than stalling, prefetch stops before playback suffers, a failed page request is retried without advancing the session twice, and a struggling feed serves a stale page rather than none.
    1. Throughput falls from 4 Mbit/s to 700 kbit/s while a 540p clip plays.
    2. With under 1 s buffered, the app switches the rest of this clip to the fallback rendition.
    3. The user keeps swiping; the heads already queued still start instantly.
    4. The next-page request times out, and the app retries with the same cursor.
    5. The feed service picks 360p for every video on the slow link.
    6. If ranking is overloaded, the feed service serves the session’s cached candidates in their last order.
    7. The signal returns; the next page arrives and the app rebuilds its prefetch window.
  5. Skips and rewatches reshape the next page: The feedback loop at three speeds. Outcomes ride on the next page request and change it at once; the stream turns them into seen filters and stats within seconds; the models learn from them overnight.
    1. The user skips three cooking videos within a second each, then watches a cycling clip twice.
    2. The app sends each outcome to the event stream in small batches.
    3. The next page request carries the same outcomes, and the feed service folds them into the session.
    4. The session has moved, so candidates are refreshed and the ranker scores them with the new session features.
    5. Within seconds the stream processor adds the plays to the seen filter and updates the videos’ stats.
    6. Overnight, training joins the day’s plays with the features served, and ships new models.

Deep dives · 16 min

Pick two. Ask the headline question, let them answer, then use the follow-ups. The follow-ups are where the level gets decided, so leave time for at least three of them.

Instant start: prefetch and small pages

Ask: The next video has to start the moment the thumb lifts. How, without burning through the user’s data plan?

Good answers name: Prefetch the first 2 s of the next 3 to 5 videos, depth by network class, from pages of 8 asked for 4 videos early, Fetch on swipe, no prefetch, Prefetch whole clips for the next few videos, Big pages of 50, prefetched deep.

Our pick: The app keeps a queue of unplayed videos and, for the first few, a head: the init segment plus the first 2 s at the rendition chosen for its network class, ~200 KB on cellular and ~375 KB on Wi-Fi, stored as its own object so it is one request. Depth is 4 on unmetered Wi-Fi, 3 on cellular, 2 on a slow link and 1 with data saver; heads are fetched in queue order and only while the playing video has 4 s buffered, so prefetch never competes with what is on screen, and the next player is created and its first frame decoded in the background. The app asks for the next page of 8 when 4 unplayed videos remain, which hides the page’s latency behind four videos of watching and keeps the feed only four plays behind the user. During playback the body is fetched in ~4-s ranges a few seconds ahead of the playhead, not whole, because a third of plays end within 3 s. Waste is the heads queued at session end (~1 MB, ~6 % of egress) plus a few seconds of buffer per swipe, and the last session’s heads stay on disk to be offered to the server at the next open, which turns most of that waste into an instant first video.

  1. Why 2 seconds for the head, not 1 or 5?
    Two seconds covers fetching the first body range on a reasonable link: 400 KB at 3 Mbit/s is about a second, plus a round trip. With a 1-s head, a slightly slow body fetch stalls a video the user did not skip. With 5 s, each head is two and a half times the bytes, and the waste at session end grows by the same factor. It is also exactly one fragment, so the head ends on a keyframe and the body starts cleanly.
  2. How do you know prefetching is working?
    The app reports, per swipe, whether the head was ready, the time to first frame, and any stall in the first 3 s, tagged by network class and queue position. The SLOs are p95 time to first frame under 150 ms and under 1 % of swipes without a ready head. Next to them sits waste: bytes fetched for heads never played and buffers dropped on swipes, as a share of all bytes, per class. A change to depth is an A/B test on those three numbers plus session length.
  3. Why not let the server re-rank the videos already queued on the phone?
    Because their heads are already downloaded, and replacing them throws that data away and costs a new head for each. The trade is explicit: the feed reacts after the queued videos, about four swipes, in exchange for instant starts. When a session turns sharply the server can still drop queued videos whose heads have not been fetched, which costs nothing, and the app can skip a queued video locally if its topic is one the user just skipped three times.
  4. You launch in a market where most users are on slow, expensive data. What changes?
    Depth drops to 1 or 2 and heads to 360p, ~100 KB, so a swipe still starts from memory on a link too slow to fetch on demand. The body buffer target rises, because throughput is erratic, and the app asks for pages earlier so a long dead zone does not empty the queue. Codecs matter more: an HEVC or AV1 copy of popular videos cuts their bytes by about a third on devices that decode it in hardware.
  5. The app opens after a night offline. What plays first?
    The heads still queued from last night, if the server confirms them. The first-page request lists them; the server checks they are still public and unseen and returns them first, followed by new videos. The first video then costs only the page round trip, ~200 ms, not a page plus a head fetch. If the check fails or is slow, the app waits for the page rather than play an unchecked video: a removed video must never play.
One rendition per clip, not mid-clip ABR

Ask: YouTube switches bitrate segment by segment. Why pick one rendition per video here, and what happens when the network changes mid-clip?

Good answers name: One rendition per video for the app’s network class, chosen at page time, with a fallback for emergencies, Standard HLS or DASH with segment-by-segment ABR, Always serve the lowest rendition, The app picks from the whole ladder for each video.

Our pick: The app reports a network class with every page request: metered or not, data saver, and the 20th-percentile throughput of its recent downloads. Each class has a cap (data saver 360p, metered 540p, unmetered up to 1080p on a large screen), and within the cap the feed service picks the highest rendition this video actually has whose bitrate is at most two-thirds of the measured throughput, per video, because a talking head at 720p may need half the bits of a dance clip. The response carries that URL and one rendition lower as a fallback. The clip plays at that rendition unless the buffer ahead falls under 1 s, when the app switches the rest of the clip to the fallback at the next 2-s fragment, which is keyframe-aligned across renditions; it never steps up mid-clip. Files are one head and one body per rendition with a fragment index in the head, so the same files serve segment-level ABR for the rare clip over a minute. On the CDN, choosing per class means two or three renditions of a popular video carry nearly all its traffic instead of five, so its heads stay hot at every edge.

  1. A user starts a 720p clip on Wi-Fi and walks out of range. What happens?
    If the link stays above the clip’s bitrate, nothing: the body is fetched a few seconds ahead and 720p needs 1.5 Mbit/s. If the buffer ahead falls under a second, the app switches to the fallback at the next fragment, a step down few people notice on a phone. The next page request carries the new class, so the following videos are chosen for cellular. Heads already fetched at 720p play as they are rather than being thrown away.
  2. Why does the server choose, when the YouTube design rejects server-side ABR?
    Server-side ABR there means the server deciding segment by segment without seeing the buffer, which is wrong. Here the server makes one choice per video before playback, from the app’s own measurement, and knows things the app does not: which renditions this video has yet, each one’s real bitrate, and the cost policy for the class. The app keeps a veto and can step down. Decisions that need the buffer stay on the phone.
  3. How many renditions do you encode, and why so few?
    Three for every video, 360p, 540p and 720p, ~10 MB for a 30-s clip, because most uploads are watched a few hundred times and every rendition is storage forever. 1080p and HEVC are added when the pool promotes a video past stage 0, about 10 % of uploads, where the egress saved pays for the encode. HEVC cuts bytes by roughly a third at equal quality on devices with hardware decoders, which on a popular video is worth far more than the encode.
  4. How would you know the class thresholds are right?
    From the app’s per-play reports: rendition, stalls, time on the fallback and watch time, by class, device and carrier. If a class sends more than a fraction of a percent of plays to the fallback, its threshold is too greedy; if an A/B test giving a class a higher rendition raises watch time with no rise in stalls, it was too cautious. Thresholds are server config, so they change per country without an app release.
Watchable within a minute

Ask: A creator taps Post. Why not encode the full ladder and then publish, and how do you reach watchable in under a minute without skipping the safety checks?

Good answers name: Upload to the nearest edge during captioning, a fast 540p lane with automated checks beside it, the rest of the ladder later, Encode the full ladder, then publish, Serve the phone’s own encode as the first rendition, Chunk-parallel encoding for every upload.

Our pick: The app encodes on the phone (1080p at ~4 Mbit/s, ~15 MB for 30 s) and starts a resumable upload to the nearest edge PoP as soon as editing ends, so for most posts the bytes have arrived before the caption. Post sends the caption and settings to the same edge with a client post id, and the edge writes the original and a manifest to the regional store, replicated, before it acknowledges. The fast lane decodes once and does two things with the frames: it encodes 540p with a keyframe every 2 s (about 2 s on 4 cores), and runs the automated checks: perceptual hash matching against known illegal content, classifiers on a frame a second, speech and caption text, and an audio fingerprint. When both report, the video service makes the video public with one rendition, restricted pending review, or removed, and the pool admits public ones to stage 0. The slow lane adds 360p and 720p within minutes at low priority, and 1080p and HEVC only when the pool promotes the video. The target is public within 60 s of upload at p95; typical is 20 to 30 s, and the checks, not the encode, are the long pole.

  1. The upload dies at 80 %. What does the creator see?
    A paused upload, not a failure: the app keeps the upload id, asks the edge which 1 MB chunks arrived, and sends the rest when the connection returns, even after a restart. The post is not acknowledged until the original is assembled and replicated, so “uploading” is honest. Incomplete uploads expire after 24 hours.
  2. The classifiers are slow today: 2 minutes instead of 15 s. What do you do?
    Keep the gate and miss the minute. Publishing first would make the first 200 test viewers the moderation system, which this design exists to prevent. Scale the classifier fleet on queue depth, keep the cheap and decisive hash matching in its own lane so known-bad content is still caught in seconds, and alert on the share of posts taking more than 60 s to go public.
  3. Why upload to an edge PoP instead of straight to the region?
    On a lossy mobile link, upload throughput is limited by round-trip time, because TCP and QUIC recover from loss one round trip at a time, and a PoP 10 ms away recovers ten times faster than a region 100 ms away. The edge relays over the provider’s backbone, which is fast and clean. It also gives one upload endpoint worldwide. The original lands in one home region, chosen by the creator’s location, which is also where its first viewers are.
  4. A creator with 20 M followers posts. Do the followers see it before review?
    Twenty million followers is a wide audience, so following must not become a way round the review gate. Accounts with a long clean record can reach followers before review at a capped rate, with their uploads first in the review queue so a person looks within minutes; accounts without that record are held to the same stage limits as anyone. The trade is minutes of delay on some posts against harm that a later takedown cannot undo.
The feedback loop in seconds

Ask: A user skips three cooking videos in a row. How quickly does the feed notice, and what actually changes: the model, or something else?

Good answers name: Session state updated on every page from outcomes in the request, stream-updated stats in seconds, models retrained daily, Train the model online from the event stream, syncing parameters to serving every minute, Precomputed feeds refreshed hourly, Send a long ranked list and re-rank it on the phone.

Our pick: Every page request carries the outcomes of the plays since the last one (id, watch ms, length, loops, skip, like, share), and the feed service folds them into the session before ranking: the session vector moves toward embeddings of completed and looped clips and away from quick skips, weighted by watch fraction, and per-topic, per-creator and per-sound counters record this session’s reactions. Those counters and the last 20 outcomes are ranker inputs, so the same model scores the same candidate differently after three skips, and when the vector has moved enough a session ANN query brings new candidates. Separately, the stream processor updates seen filters, recent history and per-video stats within seconds, which is what matters across sessions and devices and for the pool. Models retrain daily on served features joined with outcomes and ship by canary. ByteDance’s 2022 Monolith paper describes training a recommendation model online from a Kafka stream of user actions, with a collisionless embedding table (cuckoo hashing, an id getting an embedding only after it has appeared often enough, and embeddings expiring when their ids go quiet) and parameters synced to serving at minute-level intervals. I would treat that as the next step, starting with the embeddings of new videos, not the first thing to build.

  1. The stream processor falls 10 minutes behind. What breaks?
    The session’s reaction does not, because outcomes arrive with the page request. Seen filters lag, but the session’s exact served list still keeps this session’s videos out, so only a second device might see a repeat for a few minutes. Per-video stats go stale, which slows pool promotions and blunts trending. Every stream feature carries its update time, so an alert on feature age fires before users would notice.
  2. How do you stop one evening’s mood from overwriting long-term taste?
    Session state lives in the session and expires 30 minutes after the last activity, so it shifts this sitting only. The long-term vector moves only through daily training on weeks of history, where one evening of skipped cooking is a small signal. The ranker sees both and learns from data how much to trust each, and test slots keep sampling topics the session has turned away from.
  3. Why are watch fraction and loops the core labels rather than likes?
    Every play has them and likes are rare, perhaps one play in twenty. A quick skip is an explicit negative on almost every swipe, which a scrolling feed never gets. Completion has to be normalised by length, since a 10-s clip completes far more often than a 3-minute one; TikTok has said publicly that finishing a longer video is a strong signal of interest. Likes, shares and follows get heads of their own, weighted higher because they cost the user more to give.
  4. How do you count 2 M views a second and show a number on every video?
    A view is not a row. The stream processor counts views per video per 5 s, once per (viewer, video, day), and writes one increment per window, so the long tail costs one write per video per window and a viral video a few writes a second spread over 16 sub-counters summed on read. The count is shown rounded, so seconds of lag and a little loss on failover are invisible, and creator analytics are recomputed exactly from the warehouse each day.
  5. Someone runs bots that watch a video a thousand times to push it up the pool. What stops them?
    Session effects are per session, so a bot only games its own feed. For the pool, plays count once per (viewer, video, day) and only from accounts and devices with normal histories; bursts from new accounts on one network are discounted by the abuse pipeline before they reach the stage counters. Human review before 20,000 views is a second look at anything climbing suspiciously fast.
Cold start: the staged traffic pool

Ask: Thirty million videos arrive a day, most from accounts with few followers. Which get shown, to whom, and how does a great video from a nobody reach millions without a harmful one doing the same?

Good answers name: Staged pool: 200 test views to interest- and region-matched users, then 2,000, 20,000 and 200,000 for the top share, with human review before 20,000, Distribute to followers only, and let shares carry it, One bandit over all new videos, with per-video budgets, Uniform random exposure for new videos.

Our pick: About 25 M of the 30 M daily uploads are public and pass the automated checks; each enters stage 0 with a quota of 200 views. Its audience is drawn from users whose session or long-term vectors are near the video’s content embedding (frames, audio, sound, caption, hashtags), in its language and region first, at most one test video per page of 8. When the views are in, usually within the hour, the pool scores the video on completion, rewatch, share, follow and quick-skip rates against a cohort of the same length bucket and topic, using a lower confidence bound so a lucky small sample does not win, and promotes the top 10 % to 2,000 views. The top 5 % of those go to 20,000 after human review, and the top 2 % of those to 200,000, after which the video leaves the pool and competes on its own scores: ~13 B starts a day in all, about 2 of every 8. A video that misses a gate stays recommendable at a trickle, and a later surge in shares re-opens promotion. New users get the mirror image: a first page spread across popular topics in their region, then a session vector after a few plays, since 20 swipes in five minutes say a lot. TikTok has said publicly that follower count is not a direct factor in For You recommendations; the stages and numbers here are my design.

  1. What does a video with 200 views cost on the CDN, and how do you keep it low?
    About 200 × 2.5 MB = 500 MB of egress, but what hurts is cache misses: if 200 views landed on 200 PoPs, every one would miss. Drawing the test audience from the video’s own region and language first puts most views behind one shield and a handful of PoPs, so the store serves each object about once. When a video is promoted to stage 2 or 3, the pool already knows which regions its next 20,000 or 200,000 views will come from and warms its heads there first.
  2. A spammer uploads 500 near-identical videos to farm test views.
    Quotas scale with creator trust: beyond a few uploads a day, a new account’s videos get smaller test audiences, and near-duplicates (close perceptual hash and content embedding) share one test instead of getting 500. Spam classifiers in the automated checks drop the obvious cases. Trust is about abuse, not popularity, so a new honest creator still gets a full test.
  3. How do you know the gates promote the right videos?
    Look at what happens after promotion: a video promoted to stage 2 should keep its rates at stage 2, and if promoted videos regularly fall back to the cohort average, the gate is rewarding noise and the bound should be stricter. Keep a small holdout of videos that missed a gate but are promoted anyway, to measure how many good videos the gate buries. And track the share of new creators whose first video reaches 2,000 views.
  4. A pool video is removed after review. How fast does it stop playing?
    New serves stop within 30 s, when feed hosts’ cached serving records expire. Copies already queued on phones are dropped by the next page response, which names any removed ids among those the app reported as queued. Signed URLs expire after 6 hours, and for illegal content the edge purges the video’s objects by prefix, so even a saved URL fails within minutes.
  5. Ten times the uploads tomorrow. Does the pool still work?
    Stage 0 would need 50 B test views, all of the traffic, so either the quota per video shrinks or the bar for entry rises. I would cut stage 0 to 100 views for low-trust creators, let the content model’s predicted quality decide which uploads get a full test, and lean on the bandit inside the stage to spend views where they are most informative. The later stages limit themselves, because they only take a top share.
Building each page: seen sets and slots

Ask: Every page of 8 has to be fresh, mixed and never show something the user watched last week. How do you build one in about 100 ms, 250,000 times a second?

Good answers name: Four weekly Bloom filters per user plus the session’s exact served list, read with the session in one round trip, An exact table of plays per user, read on every page, One growing Bloom filter per user, A cuckoo filter per user, with deletes.

Our pick: The seen set is four Bloom filters per user, one per week, each sized from the user’s plays the week before at ~10 bits per entry for 1 % false positives (~440 B for an average week). The stream processor adds each play to the current week within seconds, and each week the oldest slice is dropped, so a video can return after four to five weeks. The session keeps an exact list of everything served in it, which covers both the seconds before the stream catches up and videos that were served but never played. A user’s keys share a hash tag, so session, seen slices and features are one round trip (~5 ms), and testing ~1,500 candidates against four in-memory filters takes well under a millisecond. The page then takes ~200 survivors through the GPU ranker (~40 ms; ~500 GPUs at peak), and the mixer fills 8 slots: one stage-0 test, up to one boosted pool video, up to one new upload from a followed account (followees read from the engagement service once per session, their uploads from the creator index), and the rest by score, never two in a row by the same creator or with the same sound, a rule TikTok has described publicly. The budget is ~5 ms state, ~30 ms candidate refresh when it runs, ~10 ms pre-rank, ~40 ms rank and ~10 ms for mixing, renditions and signing: ~100 ms, with p99 under 200 ms.

  1. The node holding a user’s seen filters is lost. What do they see?
    Repeats are the visible failure, so a missing filter is never treated as empty. The online store keeps three replicas, so losing one node changes nothing. If every copy is gone, the feed rebuilds the four slices from the user’s watch history (kept for the history page) before building the page, a few milliseconds of hashing for a typical user, and serves from the session’s cached candidates while it runs.
  2. A user deletes a video from their watch history. Should it come back in the feed?
    If the product says deleting history means forgetting, then yes, and a Bloom filter cannot remove one entry. The honest options are a small per-user “unsee” list checked before the filters, cheap because such deletes are rare, or rebuilding that user’s slices from the edited history. Both beat moving every user to a deletable structure for a rare action.
  3. Why mix followed accounts into For You at all, rather than leave them to the Following tab?
    A new upload from someone you follow is one of the strongest predictors of a watch, and many users never open the Following tab. One slot at most keeps For You from becoming chronological, and the ranker still decides whether to fill it: an account followed two years ago and always skipped loses the slot. Reading followees once per session and their recent uploads from the creator index is fan-out on read, which suits short sessions and avoids writing every upload into millions of inboxes.
  4. Ranking 400 candidates per page instead of 200 doubles GPU cost. How do you decide?
    Like any change: an A/B test on watch time and session length, set against ~500 more GPUs at peak. Before paying, check the cheaper route: if the pre-ranker’s top 200 already contains the heavy ranker’s top 8 out of 400 more than 95 % of the time, extra candidates rarely change the page. Small pages make this sharper than on a long feed, because each extra candidate is scored for 8 slots, not 50.

Close · 5 min

Ask what breaks first at ten times the load, and what they would build next. Then give them your read: one thing that was strong, one thing that was missing, one thing to practise. Be specific; “good job” helps nobody.