# Design a News Feed: Push, Pull or Both? (2026)

> A news feed looks like a database query and is really a question about where to spend work: when a post is written or when a feed is read. The prompt, both designs with numbers, the celebrity problem, the read path, and what breaks.

*Source: https://www.infowok.com/design-a-news-feed/ · Sukhveer Kaur · Published October 24, 2026*

---

The interviewer writes "Design a news feed" on the board. The candidate says: "Easy. Select the latest posts from the people the user follows, sorted by time." That query is correct, and fast on a laptop with a hundred users.

Then the interviewer asks: "Assume a user follows 200 accounts and ten million people open the app at breakfast. What does that query cost?" About 200 lookups per open, paid every time anyone looks at the screen.

The whole problem is one question: **do you build a feed when a post is written, or when a feed is read?** This lesson answers it with numbers, then covers what breaks.

<Prerequisites>

- [The system design interview framework](/system-design-interview-framework-2026/) and [back-of-envelope estimation](/back-of-envelope-estimation-primer/), because the answer depends on the numbers.
- [Caching](/caching-strategies/), [sharding and replication](/database-sharding-and-replication-explained/) and [delivery guarantees](/message-delivery-guarantees-explained/), for the deep dives.

</Prerequisites>

<Callout type="note" title="Remember from the rate limiter lesson">
Why did the limiter keep one tiny record per client instead of recomputing it from history on every request? A small precomputed answer on the hot path is cheaper. A feed list is the same trade, with one list per reader.
</Callout>

<KeyTakeaways>

- A feed is a choice of when to do work: push at write time costs per follower, pull at read time costs per account followed.
- Push makes reads one cheap lookup but does several times more writes than reads arrive, and it breaks on accounts with millions of followers.
- A hybrid pushes for ordinary authors and merges celebrity posts at read time.
- Store ids in the feed list, keep post bodies in one place, and page with cursors, not page numbers.
- The feed list may be stale, so the post store is the truth: filter deletes and blocks at read time.

</KeyTakeaways>

<LessonOutcome>

Design a news feed end to end in 45 minutes: choose between push, pull and hybrid using numbers, design the read path and pagination, and say what happens with celebrities, inactive users and deletes.

</LessonOutcome>

## Step 1: Pin Down the Problem

Ask before drawing: text only? Private accounts? One feed or several? How fresh? How many users? If the interviewer shrugs, **state an assumption aloud** and move on.

**Functional requirements:**

1. A user publishes a post.
2. A user follows and unfollows other accounts.
3. A user opens a home feed: the newest posts from the accounts they follow, newest first, 20 at a time, with a way to load more.

**Out of scope** (say these out loud):

- Media upload and video processing (a post stores a media URL; files live in blob storage behind a CDN), search, notifications, ads, comments and like counts. Each is its own problem.
- Ranking by relevance. Start with newest first, then add a ranker in deep dive 4.

**Non-functional requirements**, each with a number. These are assumptions you announce:

- Fast to read: the feed should load in a few hundred milliseconds even at the p99.
- Available: a feed that is a few seconds stale is fine, a feed that errors is not. Reads matter more than perfect freshness.
- Eventually consistent: a new post may take seconds to reach every follower. Nobody can tell.
- Private by default: a blocked or removed reader must never see a post, even from a stale list.
- Scale: assume 10 million daily users. Step 4 turns that into numbers.

## Step 2: Entities and API

```text
User     id
Follow   follower_id, followee_id
Post     id, author_id, text, time
Feed     derived: post id list

POST /posts          (auth)
  Idempotency-Key: <uuid>
  -> 201 {id}
POST /follow/{user}  -> 204
GET  /feed?cursor=&limit=20
  -> {posts[], next_cursor}
  401 no auth, 429 too many
```

The feed is **derived**. The only facts are users, follows and posts, and every design below computes the feed from them.

Index the follow table both ways. "Who does this reader follow?" (by `follower_id`) drives the pull design, and "who follows this author?" (by `followee_id`) drives the push design. Posts need an index by author and time, so "latest posts of this author" is one range read.

Retrying `POST /posts` with the same idempotency key must not create a second post. The read call takes a cursor instead of a page number. New posts keep landing at the top, so "page 2" changes under the reader, who sees repeats or misses posts. A cursor says "give me posts older than this one", which stays correct.

<Callout type="note" title="How a real system does it">
Mastodon's API works this way: it [takes `max_id`, `since_id` and `min_id`](https://github.com/mastodon/mastodon/blob/main/app/controllers/api/v1/timelines/home_controller.rb), and the feed reads an id range from Redis ([feed model](https://github.com/mastodon/mastodon/blob/main/app/models/feed.rb)). Once ranking is added, the cursor must also encode the ranked position or a snapshot id, because a score-ordered list is not stable by id.
</Callout>

## Step 3: A Design That Works

Build the simple version: look up who the reader follows, fetch each account's latest posts, merge by time and return the top 20.

![First working design, fan-out on read: a feed request looks up the 200 accounts the user follows, fetches the latest posts of each, merges by time and returns 20, costing about 200 lookups on every open](./design-a-news-feed-v1.svg)

This is correct, fresh and simple. It is called fan-out on read, or a pull design, because the reader pulls everything together.

<Callout type="tip" title="Predict first">
Assume the reader follows 200 accounts and 3,000 people open a feed in the same second. How many lookups per second is that, and is it a problem?
</Callout>

Call it **600,000 lookups a second** (3,000 × 200), each one a database read. Everything about this design costs more as users follow more accounts, and the cost lands on the reader, at the moment the reader is waiting. So the next design moves the work earlier, to when a post is written.

## Step 4: Estimate What Matters

```text
Assume 10M daily users
5 feed opens each a day

Reads:  50M / 86,400
        ≈ 580/s, peak 3,000/s
Posts:  2M a day
        ≈ 23/s, peak 115/s
Followers: 200 on average
Follows:   200 on average
Post: 1 KB (assumed)
```

The two 200s are equal because every follow is one followed and one follower gained. Single accounts are lopsided, which is where the trouble starts.

Now price the push design, where every post is copied to every follower's feed list:

```text
Inserts: 23/s x 200 = 4,600/s
Peak:   115/s x 200 = 23,000/s
Storage: 10M users x 800 ids
         x 50 B = 400 GB
```

Each line decides something:

- At ≈ **400 GB**, the feed lists are too much for one machine, so the lists must be sharded. That is the [sharding lesson](/database-sharding-and-replication-explained/) again.
- At ≈ **4,600 inserts a second**, push does about eight times the average read rate of 580 a second. Push does not remove work, it **moves** it from the reader to the writer.
- With the assumed 3,000 opens a second at peak, each loading 20 posts, the cache sees **60,000 reads a second**. That number, not the feed list, sizes the cache tier.
- Bandwidth: 60,000 × 1 KB = 60 MB/s ≈ 480 Mbit/s of text at peak. Media would dwarf it, so it goes through a CDN.
- Posts: 2M × 1 KB = 2 GB a day ≈ 730 GB a year, small next to the feed lists, so memory is the bill.
- The 50 bytes per entry is an assumption that includes overhead. Measure your own store.

![Assumed load numbers for the feed: 10 million daily users, about 580 reads a second and 3,000 at peak, about 23 posts a second, 4,600 push inserts a second, about 400 GB of stored feeds](./design-a-news-feed-scale.svg)

## Step 5: Deep Dives

Four questions decide this design. Cover them in this order, and let the interviewer steer.

### Push, Pull or Both?

<Concept href="/caching-strategies/" title="Caching strategies">
A precomputed feed is a cache of the answer to "what should this reader see?". The same questions apply: how fresh, how big, and what happens when it is empty.
</Concept>

<DesignOption tier="works" title="Fan-out on read (pull)">
Compute the feed when it is opened, as in step 3. No extra storage, always fresh, and a new follow shows up at once.

<Tradeoff>The cost grows with the number of accounts followed and lands on the reader. With the assumed 3,000 opens a second and 200 follows, that is ≈ 600,000 lookups a second.</Tradeoff>
</DesignOption>

<DesignOption tier="scales" title="Fan-out on write (push)">
When a post is published, save it once, then insert its id into the feed list of every follower. Reading is one lookup of the reader's own list. Mastodon's code does this: its [fan-out service](https://github.com/mastodon/mastodon/blob/main/app/services/fan_out_on_write_service.rb) walks the followers in batches and queues a job per follower, and each job adds the post to that follower's feed.

```text
on new post by author A:
  save post (once)
  add to own feed
  for each batch of followers:
    queue a job per follower
job(post, follower):
  ZADD feed:<follower> id id
  trim to newest 800
```

It also [inserts the post into the author's own feed](https://github.com/mastodon/mastodon/blob/main/app/services/fan_out_on_write_service.rb) first, outside the queue (a per-user setting that is on by default). That gives authors read-your-own-writes: their post is there on the next refresh while followers wait.

<Tradeoff>Writes explode: ≈ 4,600 inserts a second on average and 23,000 at peak, plus the storage. And an author with an assumed ten million followers makes ten million inserts for a single post.</Tradeoff>
</DesignOption>

![Fan-out on write: an author posts, the post is saved once, a fan-out queue and workers insert the post id into each follower's feed list, and the reader later opens their own precomputed list](./design-a-news-feed-fanout.svg)

#### What Does One Huge Account Do to Push?

This arithmetic changes the design. Assume the system can spend 20,000 inserts a second on one post:

```text
10,000,000 followers
÷ 20,000 inserts/s
= 500 s  (over 8 minutes)
```

For eight minutes some followers see the post and some do not, and every other author's fan-out waits behind it in the queue.

<DesignOption tier="survives" title="Hybrid: push for most, pull for celebrities">
Push posts from ordinary authors. For authors over a follower threshold, skip the push: save the post once, and at read time merge the reader's feed list with the latest posts of the celebrities they follow. A reader follows only a handful of such accounts, so the read-time merge stays small.

<Tradeoff>Two code paths to test and explain, and a threshold to choose. That threshold is a design judgement, so pick it from your own numbers: the follower count where one post's fan-out is too slow or too expensive.</Tradeoff>
</DesignOption>

![Hybrid design: a normal author with an assumed 200 followers is pushed into each follower's feed, a celebrity with 10 million followers is saved once and skipped by fan-out, and the reader's feed merges the precomputed list with the celebrity posts at read time](./design-a-news-feed-hybrid.svg)

<Callout type="key" title="A rule to remember">
**Push pays per follower. Pull pays per follow.** If one author has millions of followers, push is the expensive one. If one reader follows thousands of accounts, pull is the expensive one. The hybrid avoids both.
</Callout>

### Where Do Feeds Live, and How Is a Page Read?

<Concept href="/database-sharding-and-replication-explained/" title="Database sharding and replication">
Feed lists are data too. They need a shard key (the reader's id), a way to survive a lost machine, and a size limit.
</Concept>

<DesignOption tier="works" title="A table in the main database">
A `feed_items` table with one row per reader and post, indexed by reader and time.

<Tradeoff>Simple and durable, but at the assumed ≈ 4,600 inserts a second and 400 GB it competes with everything else in the database, and trimming old rows becomes its own job.</Tradeoff>
</DesignOption>

<DesignOption tier="scales" title="One sorted set per reader in Redis">
A sorted set keeps entries ordered by a score. Use the post id as member and score, which works when ids grow with time. [ZADD](https://github.com/redis/docs/blob/main/content/commands/zadd.md) is O(log N) per insert. Mastodon keeps each home feed as a sorted set and [caps it at 800 entries](https://github.com/mastodon/mastodon/blob/main/app/lib/feed_manager.rb) (current main branch, as of 2026-10-23). Your cap is your own choice.

```text
ZADD feed:42 7001 7001
ZREMRANGEBYRANK feed:42 0 -801
```

To read a page, ask for 20 ids below the cursor, newest first. [ZRANGE](https://github.com/redis/docs/blob/main/content/commands/zrange.md) takes `BYSCORE`, `REV` and `LIMIT` since Redis 6.2:

```text
ZRANGE feed:42
  (7001 -inf
  BYSCORE REV
  LIMIT 0 20
```

<Tradeoff>Memory is the cost: the cap and the active-user rule decide the bill. Old posts fall off, so "load everything since last month" needs a fallback to the database.</Tradeoff>
</DesignOption>

<DesignOption tier="survives" title="Sharded lists, replicas and ids only">
Shard the lists by reader id with consistent hashing ([lesson](/consistent-hashing-explained/)). Give each shard a replica for failover. Keep only ids in the list, not post text.

<Tradeoff>Ids make the list small, but every page needs a second step to turn 20 ids into 20 posts, which must hit a cache to stay fast.</Tradeoff>
</DesignOption>

That second step is to hydrate the ids. Keeping bodies in one place has a benefit: an edit or a delete shows up in every feed at once.

![Reading one page: the client sends a cursor, the feed service reads 20 ids from the reader's list below the cursor, loads the 20 posts from a post cache, and returns them with the next cursor](./design-a-news-feed-readpath.svg)

#### What Happens When One Post Goes Viral?

A viral post's id sits in millions of feed lists, so every page load asks the cache for the same key. That is a hot key, and adding shards does not help, because one key lives on one shard.

The fix is more copies, not more shards:

- Keep copies of hot posts on several cache nodes, and read from any of them.
- Or keep a small in-memory copy on each feed server for a few seconds.

Both trade a little staleness after an edit for spread-out load.

![A viral post is a hot key: adding shards leaves every read of post 9001 on one overloaded shard while the others sit idle, but three copies of the post on different cache nodes each take about a third of the reads](./design-a-news-feed-hotkey.svg)

#### How Does an Open App Hear About New Posts?

Polling is the pull design again. Mastodon instead [pushes the post over an open streaming connection](https://github.com/mastodon/mastodon/blob/main/app/lib/feed_manager.rb) when it lands in that reader's feed. A lighter variant sends only a "new posts" signal, and the client fetches with a cursor.

### What Breaks?

This section separates senior answers. Three things go wrong in any push design.

<DesignOption tier="works" title="Inactive users cost writes">
Pushing into feeds nobody reads wastes inserts and memory.

<Tradeoff>Skip them: Mastodon [does not push](https://github.com/mastodon/mastodon/blob/main/app/lib/feed_manager.rb) into the home feed of an account that has not signed in recently, which [defaults to 7 days](https://github.com/mastodon/mastodon/blob/main/app/models/concerns/user/activity.rb) (as of October 2026). The returning user finds a cold feed.</Tradeoff>
</DesignOption>

<DesignOption tier="scales" title="Rebuild a cold feed on return">
When a user comes back, build the list from the recent posts of the accounts they follow, the pull query from step 3 run once. Mastodon [queues that rebuild when an inactive user signs in again](https://github.com/mastodon/mastodon/blob/main/app/models/user.rb), and its [home timeline API](https://github.com/mastodon/mastodon/blob/main/app/controllers/api/v1/timelines/home_controller.rb) answers HTTP 206 (partial content) while that rebuild is running, and 200 after.

<Tradeoff>The first open after a long absence is slower or shows a short page. Rate-limit rebuilds, or a wave of returning users after an outage becomes a pull-design load spike.</Tradeoff>
</DesignOption>

<DesignOption tier="survives" title="Treat the list as a hint, not the truth">
The fan-out queue can lag, so posts appear late. Retries are safe when the insert is idempotent: adding the same member to a sorted set twice leaves one entry, because [a sorted set cannot hold the same element twice](https://github.com/redis/docs/blob/main/content/commands/zadd.md). That is the [delivery guarantees lesson](/message-delivery-guarantees-explained/) idea: at-least-once delivery plus an idempotent write.

Deletes and unfollows are the other half. Removing one post from millions of lists costs as much as the fan-out did. Mastodon does that cleanup in the background anyway: [deleting a post walks the followers](https://github.com/mastodon/mastodon/blob/main/app/services/remove_status_service.rb) and removes it from each feed, and an [unfollow removes](https://github.com/mastodon/mastodon/blob/main/app/lib/feed_manager.rb) that account's posts from the reader's list. A new follow does the reverse and merges up to 200 recent posts of the followed account in.

The cleanup lags, so the read path is the safety net. When posts are loaded, skip any that are missing or whose author is now blocked.

<Tradeoff>Pages can come back with fewer than 20 posts after filtering, so the service asks for a few extra ids or loops once more.</Tradeoff>
</DesignOption>

![Three failure cases: an inactive user is skipped and rebuilt on return, a lagging fan-out queue makes posts late but retries are safe, and deleted or unfollowed content is removed in the background and filtered when posts are loaded until then](./design-a-news-feed-failure.svg)

### Who Ranks the Feed?

A real feed is rarely just newest first. X publishes the code for its For You feed, and the [repository README](https://github.com/xai-org/x-algorithm/blob/main/README.md) (updated September 2026) describes the shape. The feed is assembled per request: one in-network source holds recent posts in memory and returns those from accounts the viewer follows. That is a pull design, made fast by keeping recent posts in RAM.

To add ranking to this lesson's design:

1. The feed list and the celebrity merge produce a few hundred **candidate** ids.
2. Filters drop duplicates, blocked and muted authors, and posts already seen.
3. A ranker scores the candidates. X's model predicts how likely the viewer is to like, reply, block and so on, then adds the predictions up with weights.
4. Visibility rules remove unsafe posts, then the service returns 20 and a cursor.

Out-of-network posts come from other sources. The README describes a retrieval step that turns the viewer and each post into embeddings and returns the nearest posts, plus a cluster-based source added in August 2026 ([vector search lessons](/embeddings-vector-search-primer/)). Ads are blended in after ranking.

Rankers keep changing: LinkedIn's engineers [describe replacing](https://arxiv.org/abs/2602.12354) a DCNv2-based feed ranker with a sequence transformer (paper first posted February 2026). In the room, say what you would optimise and how you would launch it (shadow traffic, then an A/B test).

### What Else Gets Probed?

<Callout type="note" title="Consistency, stated concretely">
This is an availability-and-latency system. If a network split cuts a region off, keep serving possibly stale feeds (the A in CAP), and even when nothing is broken choose low latency over fresh data (the L in PACELC). Spend strong consistency only where being wrong hurts: the author's own post (read-your-own-writes above) and blocks, which the read path checks against the source of truth.
</Callout>

| If this dies | What the reader sees | What you do |
|---|---|---|
| A feed-list shard | Some readers get empty lists | Fail over to its replica; if none, rebuild by pull |
| The post cache | Slower pages, more database reads | Rate-limit misses, serve a shorter page |
| The fan-out queue | Posts arrive late | Alert on lag; pull covers the gap |
| The ranker | Unranked, newest-first feed | Timeout and fall back |
| A whole region | Another region serves a cold feed | Rebuild by pull, as for returning users |

| Topic | One-line answer |
|---|---|
| Multi-region | Write a post in the author's home region, copy it to others asynchronously. Keep feed lists and the celebrity merge region-local, so no cross-region reads on the hot path |
| Security and abuse | Authenticate every call; rate-limit `POST /posts` ([the limiter lesson](/design-a-rate-limiter/)); private accounts and blocks are checked at read time |
| Privacy law | A deletion request must reach the post store, the lists and every cache; backfill jobs are the usual miss |
| Observability | SLIs: feed p99 and error rate, **fan-out lag** (post time to last follower insert), short-page rate, cache hit rate, queue depth. Alert on lag against an SLO you pick, for example under 5 seconds for 99% of posts (assumed) |
| Cost | Memory is the bill. Levers: lower the cap, skip inactive users, ids only, push only below the threshold |
| Rollout | Change the threshold or list format behind a flag. Write to old and new lists, compare, switch reads |
| New user, no follows | Show popular or suggested posts and prompt follows |
| Ads | Insert after ranking at fixed slots, outside the cursor |

## The Final Design in One Picture

![Final design: authors post through the post service which saves once, a fan-out queue pushes ids into sharded feed lists skipping celebrities, the feed service merges the list with celebrity posts, loads posts from the cache, passes the top candidates to a ranker and returns 20 posts with a cursor](./design-a-news-feed-final.svg)

Follow one post: save it once, queue fan-out unless the author is over the threshold, and insert its id into each active follower's list. Follow one read: take 20 or more ids below the cursor, add celebrity posts, load from the cache, rank, return 20 and a cursor. A cold list is rebuilt by pull.

## What Each Level Should Show

| Level | Expected in the room |
|---|---|
| Mid-level | Pull query, then push with a feed list per user. May need a hint about celebrities. |
| Senior | Prices both with numbers, finds the celebrity problem, designs a hybrid, uses cursors, and handles deletes at read time. |
| Staff+ | Cold feeds and rebuild limits, queue lag and priorities, hot posts in the cache, failure and region plans, SLOs on fan-out lag, ranking as candidates plus a ranker, and what to build first. |

## Red Flags and Green Flags

<AnswerCompare>
<Weak label="Red flag">
"Fan-out on write, because reads are faster." No numbers, no mention of celebrities, and no answer to what happens when a post is deleted.
</Weak>
<Strong label="Green flag">
"Assuming ≈ 4,600 inserts a second against 580 reads, push moves work to the writer. That breaks for an assumed ten million followers, so I'll skip the push for those and merge at read time. The list is a hint, so I filter deletes when I load the posts."
</Strong>
</AnswerCompare>

<Callout type="warning" title="The most common way to lose this round">
Picking push or pull in the first minute and defending it for 40 more. The interviewer is waiting for the celebrity, the cold feed and the delete.
</Callout>

## Try It: A Much Bigger Author

<Exercise title="One author with 5 million followers" time="15 min">

Assume an author with **5 million followers** posts three times in an hour, at 20,000 inserts a second per post.

1. How long does one post's fan-out take?
2. What goes wrong for the other authors?
3. How do you change the design?

<Hint>

Divide followers by the insert rate, then ask what else is waiting in the same queue.

</Hint>

<Expected title="One reasonable answer">

```text
5,000,000 / 20,000 = 250 s
3 posts = 750 s (12.5 min)
```

One post takes about four minutes, and three keep the queue busy for over twelve. Other authors' posts wait behind it.

Treat this account as a celebrity: save each post once and merge it at read time. A reader follows only a few such accounts, so the extra read cost is small.

</Expected>

</Exercise>

<InInterview>

Expect "What about a user with ten million followers?", "How do you page while posts arrive?", "What if the queue falls behind?" and "How would you add ranking?" Each maps to a deep dive above. Answer with a trade-off and a number.

| Follow-up | Strong answer |
|---|---|
| 10x traffic? | Add shards and cache nodes; lower the cap; raise the celebrity bar only if fan-out time allows |
| Redis is down? | Serve by pull from the post store, rate-limited; rebuild lists afterwards |
| A user follows 50,000 accounts? | Push is cheap for them; pull is not, so cap the merge or treat them as push-only |
| Reader sees a post twice? | Dedupe by id before ranking and keep a seen set |

**A 45-minute plan:** 5 requirements, 5 numbers and API, 10 simple design, 15 deep dives, 5 failures and operations, 5 questions.

</InInterview>

## Check Yourself

<CheckYourself>

<Question q="Which costs more at read time, push or pull, and what does each one's cost depend on?">

Pull costs more at read time, growing with accounts followed. Push costs more at write time, growing with the author's followers.

</Question>

<Question q="Why are page numbers a bad way to page through a feed?">

New posts shift every page, so the reader sees repeats or misses posts. A cursor always means "older than this".

</Question>

<Question q="A post is deleted. Why not remove it from every feed list?">

It may sit in millions of lists, so removal costs as much as the fan-out and finishes late. Clean up in the background, and meanwhile skip missing or blocked posts when loading a page.

</Question>

<Question q="A returning user has been away for a month. What do you do, and what changes if a thousand return at once?">

Their list was skipped, so rebuild it by pull and show what is ready. A wave of returning users is pull-style load, so cap concurrent rebuilds.

</Question>

<Question q="One post goes viral and its cache shard overloads. Why does adding shards not help, and what does?">

One key lives on one shard. Spread the reads with copies: the hot post on several cache nodes, or a short-lived copy in each feed server.

</Question>

</CheckYourself>

## In One Picture: The 60-Second Answer

"A feed is derived from users, follows and posts. With my assumed numbers, pull costs about 600,000 lookups a second and push about 4,600 inserts a second, which fails for a ten-million-follower account. So: hybrid, threshold from fan-out time.

Feeds are capped id lists in sharded Redis ([Mastodon caps at 800](https://github.com/mastodon/mastodon/blob/main/app/lib/feed_manager.rb)), paged by cursor. The list is a hint: rebuild cold feeds, filter deletes at read time. Favour availability, watch fan-out lag."

**Which of the three numbers would you reach for first in the room?** Tell us in the comments.

<NextSteps>

- **The building blocks:** [caching strategies](/caching-strategies/) and [database sharding and replication](/database-sharding-and-replication-explained/).
- **The limiter on the front door:** [design a rate limiter](/design-a-rate-limiter/).
- **Next in Module 4:** [design a chat system](/design-a-chat-system/), where messages must arrive in order and offline users must catch up.

</NextSteps>
