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- Three clocks: the session changes the next page in seconds, online training changes embeddings in minutes, dense weights change daily.
- Log what you served: features are logged at serving time and joined to labels by impression id, so training sees what serving saw.
- The model is a sparse key-value store: mostly embedding rows, sharded by (table, ID), admitted by count and expired when idle.
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 storeKey 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
endDiagramA 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 breakingData 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- Embedding rows are partitioned by a hash of (table, ID); users ~256 GB and live videos ~150 GB on the estimate, several TB in all.
- An ID earns a row only after a threshold of occurrences; until then its side features stand in.
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#
- Three clocks: the session in seconds, embeddings in minutes, dense weights daily; each fast clock covers what the slower one can't know yet.
- Every swipe is a label: watch time judged against videos of the same length, and a sub-second skip is a negative.
- Log what you served: join logged features to labels by impression id; recomputing them is training-serving skew.
- The model is a sparse store: collisionless rows keyed by full ID, admitted by count, expired when idle, sharded by (table, ID).
- Sync only what moved: touched rows every minute, dense weights nightly, and validate every batch before serving it.
- Cold start runs on features: cached seed lists for new users, staged test pools for new videos, not follower counts.
- Rules are constraints: no same creator or sound back to back, theme caps across pages, eligibility outside the score.