~learn.

TikTok's For You Feed, the whole design.

A short-video feed for a billion monthly users that turns every impression into a training example, trains online, and syncs only the touched embedding rows to serving every minute, so it learns from a swipe in seconds through the session and in minutes through the model.

7 diagrams · 8 sections · revise in ~10 min · Download PDF prints it in the light theme, diagrams included

SQ-30 · TikTok's For You Feed · design sheethimanshukukreja.in/learn/sheets/bonus-30-tiktok-for-you-feed

At a glance#

Number Value What it drives
Monthly users 1B+ (TikTok, 2021) a global feed with no follow graph
Impressions (assumed) ~45B a day · ~1M / s peak one labelled training example each
Feed requests (assumed) ~175,000 / s peak ~2,000 candidates, ~52M heavy scores / s
Model size (ByteDance's Monolith paper) several TB · ~1,000 embedding tables sharded parameter servers, collisionless tables
Sync (ByteDance's Monolith paper) sparse every minute · dense daily ~3.2 GB a minute per serving copy (estimated)
Joiner window (assumed) 15 min in memory ≈ 900 GB on-disk store for late labels
Video egress (estimated) ~12.5 Tbit/s average multi-tier CDN, first second prefetched
Moderation 97%+ of violative video removals automated (Transparency Center, Oct 2026) eligibility is a filter, not a score

Architecture#

DiagramTwo loops, one model
%% title: Two loops, one model
flowchart TD
  APP@{ icon: "sd:phone", label: "App<br/>plays · swipes · uploads" }
  GW@{ icon: "sd:gateway", label: "API gateway<br/>feed · events" }
  CDN@{ icon: "sd:cdn", label: "CDN<br/>segments · first second prefetched" }
  UP["Upload and transcode<br/>fingerprint · scan · eligibility"]:::service
  OBJ@{ icon: "sd:object-storage", label: "Video store<br/>renditions" }
  FEED["Feed service<br/>retrieve · rank · rerank"]:::hot
  SESS@{ icon: "sd:cache", label: "Session features<br/>last actions · seconds old" }
  SPS@{ icon: "sd:servers", label: "Serving PS<br/>embeddings minutes old" }
  LOG@{ icon: "tech:kafka", label: "Kafka<br/>actions · served features" }
  JOIN@{ icon: "tech:flink", label: "Flink joiner<br/>features + labels" }
  TRAIN["Online trainer<br/>training PS · collisionless tables"]:::core
  APP ==> GW
  APP ==> CDN
  APP --> UP --> OBJ --> CDN
  GW ==> FEED
  GW -->|"swipes, taps"| LOG
  FEED --> SESS
  FEED --> SPS
  FEED -->|"features served"| LOG
  LOG -.-> SESS
  LOG ==> JOIN ==> TRAIN
  TRAIN -.->|"touched rows, every minute"| SPS
  class APP client
  class GW,CDN edge
  class OBJ store
  class SESS cache
  class SPS store
  class LOG queue
  class JOIN service
DiagramZoom-in · the streaming engine
%% title: Zoom-in · the streaming engine
flowchart TD
  FK@{ icon: "tech:kafka", label: "Kafka · features<br/>by impression id" }
  AK@{ icon: "tech:kafka", label: "Kafka · actions<br/>watch · loop · skip · taps" }
  J["Joiner<br/>15 min in memory"]:::hot
  KV@{ icon: "sd:database", label: "On-disk KV<br/>features waiting longer" }
  NS["Negative sampling<br/>keep rate r recorded"]:::core
  EK@{ icon: "tech:kafka", label: "Kafka · training examples" }
  ON["Online trainer<br/>newest first"]:::core
  HD@{ icon: "sd:object-storage", label: "HDFS dump<br/>batch retraining" }
  FK ==> J
  AK ==> J
  J -.->|"miss"| KV
  J ==> NS ==> EK
  EK ==> ON
  EK --> HD
  class FK,AK,EK queue
  class KV,HD store

Key flows#

DiagramA replay and a skip change the feed, then the model
%% title: A replay and a skip change the feed, then the model
sequenceDiagram
  autonumber
  participant A as App
  participant G as Gateway
  participant F as Feed service
  participant S as Session store
  participant K as Kafka
  participant J as Joiner and trainer
  participant P as Serving PS
  A->>G: kunafa looped twice · dance skipped at 0.8 s
  G->>K: append with impression id and sequence
  K->>S: session updated in seconds
  A->>G: next page · 3 buffered · last 10 actions attached
  G->>F: feed request r9
  F->>S: read session features
  F->>P: embeddings for user and ~2,000 candidates
  F-->>A: 6 videos · mostly desserts
  F->>K: log features used for r9
  K->>J: join r9 features with labels as they settle
  J->>P: touched rows in the next minute's sync
  Note over A,P: seconds via the session · minutes via the model
DiagramA follow 45 minutes after the impression
%% title: A follow 45 minutes after the impression
sequenceDiagram
  autonumber
  participant A as App
  participant K as Kafka
  participant J as Joiner
  participant D as On-disk KV
  participant T as Online trainer
  A->>K: swipe away at 21:00:21 · full watch and a loop
  K->>J: impression r9/2 closed
  J->>T: example with watch, loop, skip, like labels
  Note over J,D: after 15 min the features move to disk for a day
  A->>K: 21:45 follow from the baker's profile · r9/2
  K->>J: slow label
  J->>D: look up r9/2 features
  alt found within the horizon
    D-->>J: features
    J->>T: follow-only example · watch heads not counted twice
  else past the horizon
    J->>J: drop the label
  end
DiagramA bad update reaches serving
%% title: A bad update reaches serving
sequenceDiagram
  autonumber
  participant T as Training shard
  participant P as Serving shard
  participant O as On-call
  T->>P: sync seq 812 · 25M rows
  P->>P: check rows for NaN and norms out of range
  alt rows look sane
    P->>P: replace row by row while serving
  else many bad rows
    P->>P: reject batch · pause this shard
    P->>O: alert
    O->>P: restore daily snapshot plus last good syncs
  end
  Note over T,P: speed of learning is also speed of breaking

Data model#

DiagramCore entities
%% title: Core entities
classDiagram
  class Impression {
    +impressionId : string · join key
    +userId : string
    +videoId : string
    +servedAt : time
    +features : logged at serving
  }
  class Labels {
    +watchSeconds : float
    +watchRatio : float · capped
    +finished : bool
    +loops : int
    +quickSkip : bool · under about 2 s
    +aboveTypical : bool · per duration bucket
    +taps : like, share, comment, follow, not interested
  }
  class TrainingExample {
    +key : impression id
    +heads : labels present
    +keepRate : float · for log odds correction
  }
  class EmbeddingRow {
    +table : user, video, creator, sound, hashtag
    +id : full 64-bit ID · no collisions
    +vector : floats
    +lastSeen : time · per-table expiry
  }
  class Video {
    +videoId : string
    +creatorId : string
    +soundId : string
    +durationSeconds : int
    +stage : test, wider, open, resting
    +fingerprintGroup : near-duplicates share a budget
  }
  class Session {
    +userId : string
    +recentActions : last 50
    +themeWindow : recent sensitive themes
    +exclusions : creators and sounds
  }
  Impression "1" --> "1" Labels : settles into
  Impression "1" --> "many" TrainingExample : fast, then slow labels
  Video "1" --> "many" Impression : shown as
  TrainingExample "many" --> "many" EmbeddingRow : trains
  Session "1" --> "many" Impression : shapes

Lifecycle#

DiagramA new video, from upload to the open feed
%% title: A new video, from upload to the open feed
stateDiagram-v2
  [*] --> UPLOADED
  UPLOADED --> PROCESSING: transcode, fingerprint
  PROCESSING --> UNDER_REVIEW: automated scans
  UNDER_REVIEW --> REMOVED: violates guidelines
  UNDER_REVIEW --> NOT_RECOMMENDED: FYF ineligible
  UNDER_REVIEW --> TEST_POOL: eligible
  TEST_POOL --> WIDER_POOL: beats its category
  TEST_POOL --> RESTING: below its category
  WIDER_POOL --> OPEN: keeps beating it
  WIDER_POOL --> RESTING: falls off
  OPEN --> UNDER_REVIEW: rising fast, re-checked
  RESTING --> [*]
  REMOVED --> [*]
  NOT_RECOMMENDED --> [*]
  class OPEN ok
  class REMOVED bad
  class UNDER_REVIEW queue

Decisions#

Decision Chose Over Because
Learning cadence Online training, sparse sync each minute Nightly batch retrain interests drift; shorter sync always helped in Monolith
Embedding storage Collisionless table, admit by count, expire idle Hashing trick shared rows blend popular and junk IDs
Training features Logged at serving, joined by impression id Recomputed in training avoids training-serving skew
What to sync Touched sparse rows each minute, dense daily Whole model several TB; dense weights move slowly
Late labels Head-specific examples from an on-disk store Wait for every label waiting makes every example late
New videos Staged test pools on evidence Follower count follower count isn't a direct factor
Feed rules Constrained re-rank, eligibility as a filter Score penalties rules are guarantees, not preferences

When it breaks#

Failure What users see Contain it with
Joiner or trainer lags a slightly staler model session path · newest first · batch backfill
Bad embeddings synced odd feeds within minutes validate rows · pause shard · snapshot rollback
Viral video, hot partition learning slows partition the log by impression id
Fake engagement junk promoted trust-weighted labels · loop caps
Sad or diet videos pile up a narrowing feed theme caps across pages · explore slot
Viral video at the edge slow starts coalesced misses · push to edge early

Say it in the interview#