Skip to main content
Now we move to the next step: a high-level design of the system. So far, we know the user’s mobile app talks to our servers with two API calls, and our data lives in three tables in the database. The question is: when Alan opens the app, how do we actually build his feed? At the simplest level we have a pull-based design: the feed is built when the user requests it.
Pull-based (fan-out-on-read) — simple workflow
  1. Alan opens the app and the app calls the GET /get-feed endpoint.
  2. The server reads the follows table to determine who Alan follows. The query returns 300 accounts.
  3. The server queries the posts table for recent posts by those 300 accounts (one lookup per account, or equivalent aggregated query).
  4. The server gathers a few hundred posts in memory, sorts them by timestamp, keeps the newest 50, and returns those 50 posts to Alan’s app.
This approach is functionally correct and simple to implement, but it has scaling and latency issues. Every time a user opens their feed we:
  • Read the follows table (one read per feed open).
  • Perform many lookups to retrieve posts for each followed account (in this example ~300 lookups per feed open).
  • Sort and truncate results in memory on each request.
Operational cost example If we receive 600 feed opens per second, that becomes:
  • 600 reads of the follows table per second, and
  • 600 × 300 = 180,000 post lookups per second.
If peak traffic is 5× the baseline, this becomes 900,000 lookups per second — a load that will slow the database and break our latency goal (we promised the feed should load within one second). Most of that work is wasted. For example, Alan may open the feed in the morning, skim some posts, close the app, and reopen it seconds later. Very little will have changed in his feed, yet we rebuild it entirely from scratch. You might think read replicas fix this, but they only replicate the same wasteful queries across more machines. What we need is a different approach. Pause and think: every feed is rebuilt from scratch on read. Is there a way to avoid rebuilding? Yes. Instead of building the feed when Alan reads it, we should build and maintain Alan’s feed ahead of time and store it. When someone John posts, we update Alan’s stored feed (if Alan follows John). In other words, change the feed only when new posts arrive from followed users. This is the fan-out-on-write approach: when a new post is created, the system pushes that post into the precomputed feed timelines of all followers. Then GET /get-feed simply reads Alan’s stored feed (fast, single read) and returns the precomputed list.
The push (fan-out-on-write) approach trades increased write work and storage for much cheaper, predictable reads and lower read latency. Precomputing timelines is ideal when reads far outnumber writes, because it eliminates per-read aggregation and sorting.
Comparing approaches
Fan-out-on-write can cause write amplification for very large accounts (e.g., celebrities with millions of followers). For such cases, consider hybrid strategies: lazy fan-out (mix of push + on-read for large fans), per-actor sharding, caching layers (CDN or in-memory caches), or using separate “fanout” queues and background workers to smooth spikes.
Key takeaways and next steps
  • For most social feeds at scale, precomputing timelines (fan-out-on-write) provides predictable low-latency reads and much less load on the read path.
  • Carefully evaluate tradeoffs: increased write work, storage, and operational complexity versus the benefits on the read path.
  • For extremely large follower graphs adopt hybrid patterns and use batching, background workers, and rate limiting to control write amplification.
Links and references

Watch Video