One User, One Million Followers: The Fanout Problem Nobody Warns You About


system-design fanout feed-design write-amplification pub-sub social-networks scalability

The Day Virat Kohli Joined Your Platform

It’s a Tuesday morning. Your social platform has been growing steadily - a few hundred thousand users, healthy engagement, the team is proud of what they’ve built.

Then it happens.

A verified account with 47 million followers on another platform signs up and posts their first message: “Hello everyone, I’m here now.”

Within 90 seconds, your servers are on fire.

Not because of reads. Not because millions of people are viewing the post. Because your system is trying to write that single post into 47 million separate inboxes - simultaneously.

Your write throughput goes from a comfortable 500 writes/sec to a catastrophic 600,000 writes/sec in the time it takes you to read this sentence. The database melts. The queue backs up. New posts from regular users stop delivering. Engineers are paged. Investors are refreshing the app.

This is the fanout problem - and it’s one of the most misunderstood scalability challenges in social feed design.

Let’s break it down from first principles and build an architecture that survives it.


What Is Fanout?

Fanout is the act of distributing one piece of content to many recipients.

When a user with 200 followers posts something, your system needs to notify or update 200 feeds. That’s manageable. When a user with 1 million followers posts, you need to update 1 million feeds. The work scales linearly with follower count - and that’s the trap.

There are two fundamental fanout models, and the choice between them defines your entire feed architecture.


Model 1: Fanout on Write (Push Model)

When a post is created, immediately write it into every follower’s feed.

Fanout on write: a single post fans out from the Fanout Service into N individual feed stores

Each follower has a pre-computed feed in a fast store (usually Redis sorted sets). When they open the app, their feed is already there - reads are instant.

The good:

  • Feed reads are O(1) - just fetch a pre-built list
  • Low read latency - great for the consumer experience
  • Simple read path

The bad:

  • Write amplification is brutal for high-follower accounts
  • 1 post from a celebrity = 1 million writes
  • If the celebrity posts 10 times a day, that’s 10 million writes
  • Storage cost grows with follower count

Where it breaks:

Normal user (500 followers):
1 post -> 500 writes -> totally fine

Celebrity (5M followers):
1 post -> 5,000,000 writes -> database on fire

Model 2: Fanout on Read (Pull Model)

Don’t write anywhere. When a follower opens their feed, fetch recent posts from everyone they follow on demand.

Fanout on read: feed load triggers N queries to fetch posts from every followed user

The good:

  • Zero write amplification - celebrities are cheap to handle
  • Storage efficient - posts live in one place
  • Simple write path

The bad:

  • Feed reads are expensive - O(following count)
  • Following 500 people? That’s 500 queries merged and sorted per load
  • Latency is high, especially as following count grows
  • Heavy read load, hard to cache effectively

Where it breaks:

User follows 1,000 accounts:
Feed load = 1,000 queries + merge sort -> 3-5 second load time

Neither model alone is good enough at scale. Every major social platform - Twitter, Instagram, Facebook - converged on the same answer.


Model 3: The Hybrid Architecture (What Actually Works)

The insight is simple: not all users are equal.

A regular user with 500 followers? Fan out on write. The amplification is trivial.

A celebrity with 5 million followers? Fan out on read. Merge their posts at read time.

Hybrid decision: posts from celebrity accounts are stored in author timeline only; regular users are fanned out to all follower feeds

At read time, the Feed Reader merges both sources: pre-built pushed feeds for regular users, and celebrity timelines fetched on demand.

Read time merge: Redis Feed Store and Celebrity Timelines are merged and sorted before returning to the user

This is exactly how Twitter (now X) designed Timeline. They call the celebrity threshold accounts “high-fan-out users” and handle them separately.


The Full Architecture

Let’s put every component in place.

Full fanout architecture: Client to API Gateway to Post Service, which writes to Posts DB and publishes to Fanout Queue. Fanout Worker routes to Feed Store (Redis) or Celebrity Timeline (Redis). Feed Reader merges both and returns the final feed.

Component Deep Dive

The Post Service

The write path is intentionally thin. It does two things:

  1. Persist the post to the Posts DB (source of truth)
  2. Publish a PostCreated event to the fanout queue
{
  "event": "PostCreated",
  "postId": "p_abc123",
  "authorId": "u_virat",
  "createdAt": "2026-05-21T09:00:00Z",
  "followerCount": 4700000
}

It does not do the fanout itself. The API response returns immediately. Fanout is always async.

The Fanout Worker

This is where the classification happens.

function handlePostCreated(event):
  author = getUser(event.authorId)

  if author.followerCount > CELEBRITY_THRESHOLD:
    addToCelebrityTimeline(author.id, event.postId)
    return

  followers = getFollowerIds(author.id)  # paginated

  for batch in chunks(followers, size=1000):
    for followerId in batch:
      addToUserFeed(followerId, event.postId, event.createdAt)

The CELEBRITY_THRESHOLD is tunable. Twitter reportedly used around 1 million followers. You set it based on your write budget.

Pagination matters here. Even a “regular” user can have 500K followers. The fanout worker fetches followers in batches of 1,000 and pushes to the feed store. This work is distributed across many worker instances.

The Feed Store (Redis Sorted Sets)

Each user’s feed is a Redis sorted set, keyed by feed:{userId}, where the score is the post’s timestamp.

ZADD feed:user_b 1716282000 post_abc
ZADD feed:user_b 1716281800 post_xyz
ZADD feed:user_b 1716281500 post_def

// Read feed (newest first, top 20)
ZREVRANGE feed:user_b 0 19

Feed trimming keeps memory bounded:

// After each write, trim to last 800 posts
ZREMRANGEBYRANK feed:user_b 0 -801

800 posts per user is plenty. Nobody scrolls that far.

The Feed Reader (Merge at Read Time)

function getUserFeed(userId, page):
  regularPostIds = redis.ZREVRANGE("feed:" + userId, page*20, page*20+19)

  celebrities = getCelebrityFollows(userId)

  celebPostIds = []
  for celeb in celebrities:
    recentPosts = redis.ZREVRANGE("celeb_timeline:" + celeb.id, 0, 19)
    celebPostIds.extend(recentPosts)

  allPostIds = deduplicate(regularPostIds + celebPostIds)
  allPostIds = sortByTimestamp(allPostIds)
  allPostIds = allPostIds[0:20]

  posts = batchFetch(allPostIds)
  return posts

The merge is done in application memory - it’s just sorting a small list of IDs. Fast and cheap.


Handling Follower List Storage

For the fanout worker to push to all followers, it needs to retrieve them efficiently. This is a separate, non-trivial problem.

Option 1: Social Graph in Postgres

CREATE TABLE follows (
  follower_id BIGINT,
  followee_id BIGINT,
  created_at TIMESTAMPTZ,
  PRIMARY KEY (follower_id, followee_id)
);

CREATE INDEX idx_followee ON follows(followee_id);

Works well up to ~100M follow relationships. The followee_id index lets you paginate followers efficiently.

Option 2: Social Graph in a Dedicated Store

At scale, companies use dedicated graph databases or specialized stores for follower lists. Instagram famously moved their social graph to a custom service. The principle: don’t make your main Postgres do expensive SELECT follower_id WHERE followee_id = X queries during a celebrity fanout.


The Celebrity Detection Problem

How do you know when someone crosses the celebrity threshold?

Static threshold: Anyone above N followers is a celebrity. Simple but requires recomputing when users gain/lose followers.

Event-driven reclassification:

When follow_count crosses CELEBRITY_THRESHOLD:
  1. Mark user as celebrity in Users table
  2. Trigger a backfill job:
     - Remove this user's posts from all follower feeds
     - (saves Redis memory, will be read on-demand going forward)

When a user loses celebrity status (mass unfollows):

Mark user as regular
Trigger a forward-fill job:
  - Fan out their last N posts to all current followers

The transitions are edge cases but important to handle gracefully.


What About New Followers?

User B follows User A (celebrity) at 3 PM. User A last posted at 2 PM. Will User B see that post?

Yes - because the celebrity timeline cache holds the last N posts from User A, and the feed reader pulls from it at read time regardless of when User B started following.

For regular users, new followers get an empty feed for historical posts (this is intentional and common) but will receive all future posts via fanout.


Scaling the Fanout Worker

The fanout worker is the most write-intensive component. Scaling it:

TechniqueWhat it solves
Multiple consumer instancesParallelise across many users’ followers
Partition queue by author IDEnsure order within one author’s posts
Batch Redis writesReduce round trips - pipeline 1,000 ZADDs in one call
Prioritise queue by follower countProcess small fanouts first for faster delivery
Backpressure from RedisIf Redis is slow, reduce consumer throughput rather than crash

A single fanout worker can realistically process ~50,000 feed writes per second using Redis pipelines. Across 20 workers: 1M writes/sec. A celebrity with 5M followers gets their posts fanned out in about 5 seconds. Acceptable.


System Design Comparison Table

AspectFanout on WriteFanout on ReadHybrid
Write complexityHighLowMedium
Read complexityLowHighLow-Medium
Write latencyBackground (async)NoneBackground
Read latencyVery lowHighLow
Storage costHigh (per follower)LowMedium
Celebrity handlingCatastrophicFinePurpose-built
Best forSmall follower countsRead-heavy, no celebsReal social platforms

Key Takeaways

  • Fanout on write breaks when a single user has millions of followers - write amplification is unbounded
  • Fanout on read breaks when users follow thousands of accounts - read time is too expensive
  • Hybrid is the answer: push for regular users, pull for celebrities at read time
  • The celebrity threshold is a knob you tune based on your write budget
  • Feed stores are not your source of truth - the Posts DB is. Feeds are derived, ephemeral caches
  • Async fanout is non-negotiable - the post API should never block on follower list iteration
  • Redis sorted sets are the natural data structure for pre-built feeds

The architecture that handles 200 followers is not the architecture that handles 5 million. Design for the outlier - because on a social platform, the outlier is usually your most important user.


Frequently Asked Questions

Q: What does Twitter/X actually use? Twitter’s early architecture used pure fanout-on-write. As the platform grew, they moved to a hybrid model, with a concept they called “Timeline Service.” High-follower accounts are pulled at read time. This was widely discussed in their engineering blog posts around 2013.

Q: How does Instagram handle this? Instagram uses a similar hybrid model and famously moved their social graph to a custom service. Their feed ranking layer adds an ML-based relevance sort on top of the chronological merge.

Q: What if a celebrity follows another celebrity? Both are in each other’s celebrity timeline pull. When User B (who follows both celebrities) loads their feed, the merge step includes both celebrity timelines. The merge is just sorting IDs in memory - the fact that they’re both celebrities doesn’t change the logic.

Q: How do you handle feed consistency - what if a post is deleted? Deletion is hard in fanout architectures. The approach: mark the post as deleted in the Posts DB. At read time (hydration step), deleted posts are filtered out before returning to the client. Feed stores may still hold the post ID briefly, but it gets filtered at hydration. For GDPR/hard deletes, run a background job to purge from feed stores.

Q: Can you use Kafka instead of SQS for the fanout queue? Yes, and many companies do. Kafka’s advantages: replay capability, high throughput, consumer group semantics. The tradeoff: operational complexity. For the fanout use case specifically, partitioning by authorId ensures posts from the same author are processed in order. SQS FIFO with MessageGroupId = authorId gives you the same property with less ops overhead.

Q: What is the typical Redis memory footprint for feed storage? Each feed entry is roughly 16 bytes (an 8-byte post ID + 8-byte score/timestamp). A user feed capped at 800 posts uses ~12KB. For 10M users: ~120GB of Redis memory. That’s a Redis cluster spread across a few nodes - manageable and cost-effective for the read latency you get in return.


Interview Questions

Q: Design a social media feed for a platform expecting a mix of regular users and celebrities. Expected depth: Hybrid push/pull model, celebrity threshold classification, async fanout queue, Redis sorted sets for feed storage, read-time merge, hydration step, follower graph storage.

Q: What happens to your fanout architecture when a user gains 10 million followers overnight? Expected depth: Celebrity reclassification event, backfill job to remove pre-pushed posts from feeds, transition to read-time pull, handling the transition window gracefully.

Q: How would you ensure a user always sees their own post immediately after posting, even before fanout completes? Expected depth: Read-your-writes consistency - the Post Service can write to the author’s own feed synchronously (single write), fanout to others is async. Author always sees their post instantly.

Q: How do you handle feed ordering when using hybrid push/pull - what if a celebrity post timestamp is older than the last pushed post in the feed? Expected depth: The merge step sorts all post IDs by timestamp regardless of source. The final sorted list is what the user sees. The merge is done in application memory on a small list, so ordering across sources is trivially correct.

Q: How would you add ranked/algorithmic feeds on top of this architecture? Expected depth: The chronological merge is the input to the ranking layer. A separate Ranking Service scores each post (engagement signals, user affinity, recency decay) and re-orders before returning. The feed store remains chronological; ranking is applied at read time. Scores can be cached for active sessions.