Fan-out on Write vs Read
How social feeds are built — and the hybrid that handles celebrities with millions of followers.
Fan-out on write and fan-out on read are the two classic ways to build a social feed, notification inbox, activity stream, or personalized timeline. The decision is simple to state: do the work when someone publishes, or do the work when someone opens the feed?
The problem: timeline reads and writes scale differently
A feed is a many-to-many problem. One author has many followers, and one reader follows many authors. If Alice follows 800 people, her home feed needs to merge recent posts from 800 sources. If a celebrity has 200 million followers, one post may need to appear in 200 million feeds. Neither extreme is safe to handle naively.
// Fan-out on write: pay when the author publishes.
POST /posts
save post p
for follower in followers(author):
LPUSH timeline:{follower} p
GET /home
return LRANGE timeline:{viewer} 0 499
// Fan-out on read: pay when the viewer opens the feed.
GET /home
authors = following(viewer)
posts = fetchRecentPosts(authors)
return mergeRankAndTrim(posts)The right answer depends on read/write ratio, follower distribution, freshness requirements, ranking complexity, and acceptable storage duplication. Production social systems usually combine both models.
Fan-out on write: push into precomputed timelines
In the push model, publishing a post triggers a background job that inserts that post ID into each follower timeline. Reads are then cheap: load the viewer timeline from Redis or a feed store, hydrate the post IDs, rank or filter if needed, and return the page.
- Fast reads: the expensive follower expansion already happened, so opening the app can be a single timeline lookup.
- Write amplification: one post becomes many timeline writes. A user with 5,000 followers creates 5,000 feed insertions.
- Storage duplication: the same post ID is copied into many precomputed lists. Usually this is acceptable because lists store IDs, not the full post body.
// Author 42 publishes post 9001.
followers = [7, 8, 9, ...]
for followerId in followers:
LPUSH home_timeline:{followerId} 9001
LTRIM home_timeline:{followerId} 0 999
// Viewer 7 opens the app.
postIds = LRANGE home_timeline:7 0 49
posts = MGET post:{id} for id in postIdsFan-out on read: assemble when the viewer asks
In the pull model, publishing saves the post once. When a viewer opens the feed, the service fetches recent posts from everyone the viewer follows, merges them by time and ranking features, filters blocked or muted authors, and returns the top items.
| Dimension | Fan-out on write / push | Fan-out on read / pull |
|---|---|---|
| Post creation | Expands to every follower timeline | Writes one post record only |
| Home feed read | Reads a precomputed list, then hydrates posts | Fetches many author streams, merges, ranks, and trims |
| Best for | Normal users with bounded follower counts and high read volume | Authors with huge audiences or systems with rare reads |
| Main failure mode | Celebrity post creates massive write amplification | Viewer following many authors creates slow feed assembly |
| Storage | Duplicates post IDs across follower timelines | Stores each post once, but spends CPU on reads |
Pull is attractive for long-tail authors and cold feeds. If most users rarely open the app, pushing every post to every inactive timeline wastes work. Pull delays the cost until a human actually asks for the feed.
The celebrity problem and the hybrid answer
Pure push breaks on hot authors. A celebrity, brand, or emergency alert account with 200 million followers cannot synchronously write one post into 200 million timelines. Even if the writes are asynchronous, the queue backlog, Redis pressure, and storage churn can overwhelm the system.
The common production fix is a hybrid feed. Push normal authors into follower timelines. Do not push celebrity or hot-user posts into every follower timeline. Instead, keep celebrity posts in author streams and pull them at read time for viewers who follow those authors.
function readHome(viewer):
// Precomputed push timeline for normal authors.
base = redis.lrange("home_timeline:" + viewer, 0, 499)
// Small set: celebrity accounts this viewer follows.
hotAuthors = getHotAuthorsFollowedBy(viewer)
hotPosts = []
for author in hotAuthors:
hotPosts += redis.lrange("author_posts:" + author, 0, 20)
return rankAndMerge(base, hotPosts).take(50)This is also where two-stage fan-out appears: a lightweight first pass delivers IDs quickly, and a second stage enriches, ranks, or backfills. Hot celebrity streams often need hot-key mitigation because millions of readers may request the same author feed.
Ranking, freshness, and consistency gotchas
- Deletes and privacy changes: a deleted post may already exist in many precomputed timelines. Reads must hydrate by ID and re-check visibility, or a cleanup job must remove stale references.
- Follow and unfollow: when a viewer follows someone new, you may backfill recent posts into the timeline. On unfollow, you may lazily filter those posts at read time rather than remove every old entry immediately.
- Ranking changes: precomputed chronological lists are easy to cache. Machine-learned ranking may still need read-time features, hydration, and reordering.
- Duplicate posts: hybrid feeds can see the same post from base timeline and hot-author pull. Deduplicate by post ID before ranking.
- Backpressure: fan-out workers must be rate-limited. If queues grow, degrade gracefully by delaying low-priority fan-out or switching selected authors to pull mode temporarily.
Real-world examples
Social networks, team chat, notification centers, activity streams, and news feeds all use this trade-off. Twitter-like timelines often push ordinary users and pull celebrities. A chat app may push unread counters and recent message IDs into per-user inboxes. A notification system may push critical alerts but pull low-priority activity on demand.
| System | Likely pattern | Reason |
|---|---|---|
| Home timeline | Hybrid push plus pull | Fast reads for normal graph; celebrity posts avoid write storms |
| Email inbox | Write-time delivery | Each message belongs in recipient mailboxes and reads must be fast |
| Analytics activity feed | Read-time assembly | Reads are rare and freshness can be computed on demand |
- Fan-out on write pushes each post into follower timelines at publish time, making reads fast but writes expensive.
- Fan-out on read stores the post once and assembles the feed on demand, making writes cheap but reads expensive.
- Pure push breaks for celebrities because one post can trigger millions of timeline writes.
- Production feeds are usually hybrid: push normal authors, pull hot authors at read time, then merge and rank.
- Precomputed Redis timelines are fast recent windows; reads still need hydration, visibility checks, deduplication, and ranking.
Mark it complete to track your progress through the workbook.