Whiteboard ScaleNews FeedCheat Sheet
Cheat Sheet

News Feed Cheat Sheet

Key concepts, trade-offs, and quick-reference notes for your interview prep.

Fanout-on-Write vs Fanout-on-Read

#1
We chose a hybrid fanout model (not pure push or pure pull) because neither alone works at Twitter scale. Fanout-on-write (push): when a user tweets, we write the tweet ID into every follower's timeline cache. Read is O(1)O(1): fetch the pre-built cache. Fanout-on-read (pull): we do nothing at write time and build the timeline on demand by querying each followee's tweets. Write is O(1)O(1), but read is O(N)O(N) where NN = number of followees. Trade-off: push makes reads fast at the cost of write amplification; pull makes writes fast at the cost of slow reads.

💡 Twitter uses push for normal users and pull for celebrities. State the hybrid model early in the interview to show you understand both trade-offs.

Celebrity Threshold: 10K Followers

#2
We chose 10K followers as the threshold (not 1M or 100K) because a single tweet from a 30M-follower celebrity would trigger 30M cache writes. At 1KB per write, that is 30 GB of write bandwidth for one tweet. By pulling celebrity tweets at read time instead, we trade 30M writes for a few hundred reads (merge step). Trade-off: celebrity tweets appear 1-2 seconds later in followers' timelines because they are merged at read time, not pre-cached.
Setting the threshold too high (e.g., 1M). Users with 100K followers still cause 100K writes per tweet, which is 100x the normal fanout cost.

Snowflake ID: Time-Sorted Unique IDs

#3
We chose Snowflake IDs (not AUTO_INCREMENT or UUIDs) because we need time-sorted, globally unique IDs across 64 shards with zero coordination. A 64-bit integer: 41 bits for millisecond timestamp (69 years), 10 bits for machine ID (1,024 servers), 12 bits for per-machine sequence (4,096 IDs/ms/machine). Total capacity: 1,024×4,096×1,000=4.19 billion IDs/sec1{,}024 \times 4{,}096 \times 1{,}000 = 4.19\text{ billion IDs/sec}. Trade-off: IDs leak creation time (the timestamp is visible), which may be a privacy concern for some applications.

💡 Snowflake IDs make timeline sorting free: ORDER BY id is the same as ORDER BY created_at, but without a secondary index on timestamp.

Timeline Cache: 800 Tweet IDs per User

#4
We store each user's home timeline as a Redis sorted set holding the latest 800 tweet IDs (not full tweet objects), scored by Snowflake ID. Each entry is 8 bytes (tweet ID). Memory per user: 800×8B=6.4 KB800 \times 8\text{B} = 6.4\text{ KB}. For 200M users: 200M×6.4 KB=1.28 TB200M \times 6.4\text{ KB} = 1.28\text{ TB}. We chose 800 (not 200 or 2000) because most users never scroll past 200 tweets, and 800 gives 4x buffer for heavy scrollers without wasting memory. Trade-off: users who scroll beyond 800 tweets hit a cold path that rebuilds from the database.
Storing full tweet objects (1 KB each) in the timeline cache. That is 800 x 1 KB = 800 KB per user, 125x more memory. Store IDs only, hydrate from tweet cache on read.

Social Graph: Redis SET + MySQL

#5
We store the follow relationship in two places (not one) because neither store handles both durability and speed. MySQL follows table: source of truth with columns (follower_id, followee_id, created_at). Redis SET: per-user follower and followee sets for fast lookups. To check 'does user A follow user B?', we query Redis SISMEMBER in O(1)O(1). For fanout, we query Redis SMEMBERS. Trade-off: we accept the operational complexity of keeping two stores in sync via Kafka events. If Redis loses data, we rebuild from MySQL.

💡 Never write to Redis alone. Always write to MySQL first, then propagate to Redis via Kafka events.

Feed Ranking: Engagement Score

#6
We chose a weighted engagement score (not pure reverse-chronological order) because raw time ordering misses high-quality tweets posted hours ago. Formula: score=w1×likes+w2×retweets+w3×repliesw4×age_hours\text{score} = w_1 \times \text{likes} + w_2 \times \text{retweets} + w_3 \times \text{replies} - w_4 \times \text{age\_hours}. Typical weights: w1=1w_1 = 1, w2=3w_2 = 3, w3=2w_3 = 2, w4=0.5w_4 = 0.5. Example: a tweet with 100 likes, 20 retweets, and 10 replies posted 2 hours ago: 100+60+201=179100 + 60 + 20 - 1 = 179. Trade-off: we sacrifice strict chronological ordering for relevance, which some users dislike.

💡 We apply ranking at read time on the fetched timeline, not during fanout. Keep fanout simple (append ID). Rank on the way out.

Media: S3/CDN, Tweet Stores URL Only

#7
We chose to store media in S3 with CDN delivery (not inline in the tweet row) because a single 5 MB image would be 5,000x the size of a text tweet, blowing up storage and replication bandwidth. Images (up to 5 MB) and videos (up to 512 MB) upload to S3, then get a CDN URL. The tweet row stores only the URL pointer (200 bytes). Trade-off: we accept a two-phase upload flow (media first, then tweet with URL attached), which adds client-side complexity.

💡 Upload media first, get the URL, then create the tweet with the URL attached. Two-phase: media upload is a separate API call.

Write Amplification: 1 Tweet = N Cache Writes

#8
When a user with 500 followers tweets, our fanout service writes that tweet ID into 500 timeline caches. That is 500x write amplification. At 500M tweets/day and an average of 200 followers: 500M×200=100 billion cache writes/day500M \times 200 = 100\text{ billion cache writes/day}. That is 100B/86,400=1.16M100B / 86{,}400 = 1.16M cache writes per second. We chose to accept this cost for normal users (not celebrities) because O(1)O(1) reads are worth the write amplification when follower count stays under 10K. Trade-off: the fanout queue becomes the throughput bottleneck of the entire system.
Not measuring or capping write amplification. A viral tweet from a 1M-follower account triggers 1M writes, backing up the fanout queue and delaying all other users' deliveries.

Read Path: Merge Cached + Celebrity Tweets

#9
When a user opens their timeline, we: (1) fetch 800 tweet IDs from their pre-built timeline cache, (2) for each celebrity they follow, fetch that celebrity's latest 20 tweets from the celebrity's tweet list, (3) merge and sort all IDs by Snowflake ID, (4) take the top N for the requested page, (5) hydrate: batch-fetch full tweet objects from the tweet cache using MGET. Average user follows ~5 celebrities, so the merge adds 100 IDs. Total operation: 2 Redis reads + 1 batch MGET. We target under 200ms end-to-end. Trade-off: the merge step is why celebrity tweets do not appear instantly. There is a 1-2 second delay while the tweet propagates to the celebrity's tweet list.

💡 This merge-at-read-time cost is the price we pay for not fanning out celebrity tweets. For 99% of users, it adds <10ms.

Shard Tweets by user_id for Write Locality

#10
We shard the tweets table by user_id (not tweet_id) because the two most common queries are: (1) 'get all tweets by user X' (profile page), (2) 'write a new tweet for user X'. Both hit a single shard when sharded by user_id. If we sharded by tweet_id, 'get all tweets by user X' would become a scatter-gather across all shards because user X's tweets would be spread randomly. Trade-off: we accept uneven shard distribution (celebrity shards hold more data), mitigated with read replicas for heavy-read shards. With 500M tweets/day across 64 shards, each shard handles \sim8M writes/day.
Sharding by tweet_id for even distribution. Distribution is perfect, but every user-profile query fans out to all 64 shards, adding 10-20ms of aggregation latency.