This post will talk about how every feed in our social medias are designed. From the user’s side, a feed looks simple: open a page, see recent updates, keep scrolling, infinitely.
But behind that list, there is an improtant design decision: when should the system assemble each person’s feed?
It can do most of the work when someone publishes a post, or wait until a reader opens their feed. Those approaches are usually called fan-out on write and fan-out on read.
The right choice is not picking one, as each moves work to a different part of the system, and the better fit depends on how often people post, how often feeds are read, and how unevenly followers are distributed.
First, what is being fanned out?
Let's say Alice publishes a post and 200 people follow her. The system needs to make that post available in those 200 people’s feeds. It could:
- add a reference to Alice’s post to each follower’s feed when she publishes it; or
- leave the post in Alice’s own collection and find it when each follower opens their feed.
The post itself remains in one canonical record either way. A precomputed feed often stores post IDs or references, rather than a full duplicate of every post. So the design question is not whether a feed needs aggregation (it always needs it), it is when that aggregation happens.
Fan-out on write: prepare the feed ahead of time
Fan-out on write needs a prerequisite: each user has its own feed collection, referencing followed users' posts. That feed is persisted in the storage that suits most your use case (database, cache, memory). When a user publishes a post, the system creates new feed entries for its followers:
publish(post):
storeCanonicalPost(post)
for follower in followersOf(post.author):
addReferenceToFeed(follower, post.id)
When a reader opens their feed, the system retrieves a list that is already assembled. That makes the read path easier to keep fast and predictable. This method is optimal when people read feeds frequently and most authors have a manageable amount of followers.
On the other hand, the cost moves to publishing. One post by an account with 200 followers may create around 200 feed entries, same for a post by an account with millions of followers. It also do work for followers who rarely open the feed. And when a post is deleted or access changes, every stored reference may need to be removed or checked against current permissions.
In real production systems, those issues are mitigated by making the fan-out asynchronous. The post is stored first, then background workers create references to followers’ feeds. This keeps a large delivery job off the request that publishes the post, with the cost of a briefly outdated feed.
Fan-out on read: assemble the feed when it is requested
With fan-out on read, publishing stores the post once. When someone opens their feed, the system fetches recent posts from the accounts they follow, then merges and ranks the results:
getFeed(reader):
authors = followedAuthors(reader)
candidates = fetchRecentPosts(authors)
return rankAndLimit(candidates)
This avoids all the costs explored in fan-out on write approach. It can be a good fit when writes are frequent, many followers are inactive, or ranking rules change often.
But the cost is non-negligible, since the work appears on the read path instead. A reader who follows many accounts may require the system to fetch and combine a large set of recent posts for each request. That can increase database load and make latency less predictable.
The implementation matters here. “One query per followed account” is not the only possible design; indexes, batching, caching, and the way data is partitioned all affect the cost. But the system still has to gather enough candidate posts to build a useful feed at request time.
The trade-off
| Fan-out on write | Fan-out on read | |
|---|---|---|
| Main work happens | When a post is published | When a feed is requested |
| Publish cost | Grows with follower count | Usually stays close to one canonical write |
| Feed read | Starts from a prepared list | Must gather and combine candidates |
| Storage | More feed references to maintain | Fewer precomputed feed entries |
| Useful when | Reads are frequent and audiences are manageable | Writes are frequent, audiences are uneven, or ranking changes often |
| Main pressure | Large authors create write spikes | Large follow lists make reads more expensive |
This table describes where the work moves. It doesn’t guarantee a particular latency or storage cost. Those depend on the data model, indexes, caching, ranking, and delivery requirements.
Why many systems use a hybrid
Follower counts tend to be uneven. Most authors may have a modest audience, while a small number have very large ones. That makes a hybrid practical:
- Fan out posts from ordinary accounts on write.
- Keep posts from very large accounts in their own collections.
- Fetch those high-follower posts when a reader opens their feed.
- Merge both sets of candidates before returning the feed.
The hybrid approach avoids pushing every post to a huge audience immediately, while also avoiding a read-time fetch from every single followed account. There is no universal follower-count threshold. The useful cutoff depends on your workload: how frequently authors post, how often their followers read, how much read latency you can afford, and how much asynchronous write work your system can absorb.
Ask questions before choosing
- What is the read-to-write ratio? If feeds are opened far more often than posts are published, precomputing some work may pay off.
- How skewed are follower counts? A few exceptionally large accounts can make a pure write-time strategy costly.
- How fresh must the feed be? Asynchronous fan-out can introduce a short delay; this is typically accpetable for large feeds.
- How often does ranking change? If feed order depends on changing relevance scores, rebuilding stored feeds can become part of the cost.
- How do deletes and permissions work? This can be tricky, since a feed entry should not make content visible after the underlying post or access rights have changed.
I recently worked on a project where such decisions were required, since the project is new an not yet extensively used, we selected fan-out on read while we collect enough metrics to evaluate when and how to optimize: measure where the work happens, then optimize the bottleneck the product actually has.
What would you choose for a feed where most accounts have a small audience, but a few have millions of followers? Let's discuss in comments!
Top comments (0)