Lesson 317 min read

Design a News Feed (Twitter/Instagram)

April 18, 2026

// TL;DR

Design a news feed serving billions of reads a day: precompute each user's feed by fanning out new posts into per-user feed caches (fan-out on write) for fast reads, but pull celebrity posts at read time and merge (fan-out on read) to avoid the millions-of-writes blow-up. Store post IDs in a Redis list per user, hydrate at read, rank chronologically or with an ML score, and paginate with cursors.

Design a News Feed (Twitter/Instagram)

“Design Twitter’s timeline” (or Instagram’s feed, or Facebook’s News Feed) is one of the most common system design questions, and for good reason: it forces you to confront the single most important trade-off in read-heavy systems — do you do the work at write time or at read time? Get the fan-out discussion right and you have essentially passed this question.

Understanding the Problem

A news feed shows a user a continuously updated list of posts from the accounts they follow, newest-first (or ranked). The hard part is not building the feature — it is serving it to hundreds of millions of people at once.

Functional Requirements

  1. Publish a post — A user creates a post (text, image, video) visible to their followers
  2. View the feed — A user sees a feed of recent posts from accounts they follow
  3. Follow / unfollow — Users build a social graph by following others
  4. Pagination — Infinite scroll through older posts
  5. Ranking — Feed ordered chronologically or by relevance

Out of scope for a 45-minute interview: comments, likes, direct messages, notifications (each is its own design).

Non-Functional Requirements

Requirement Target
Feed read latency < 200 ms (p99) — this is the product
Feed freshness New posts visible within a few seconds (eventual is fine)
Availability 99.99% — a down feed is a down product
Read:write ratio ~100:1 — people read far more than they post
Scale 500M DAU, avg 200 follows, 2M posts/sec read, ~20K posts/sec write

Back-of-the-envelope: 500M DAU each refreshing their feed a few times a day is on the order of a few million feed reads per second at peak. Writes are far rarer — tens of thousands of new posts per second. This lopsided ratio is the reason we optimize relentlessly for read speed, even at the cost of extra write work.

Core Entities and APIs

Data Model

CREATE TABLE users (
    id          BIGINT PRIMARY KEY,
    handle      VARCHAR(50) UNIQUE,
    name        VARCHAR(100),
    created_at  TIMESTAMP DEFAULT NOW()
);

CREATE TABLE posts (
    id          BIGINT PRIMARY KEY,   -- time-sortable (Snowflake) id
    author_id   BIGINT REFERENCES users(id),
    content     TEXT,
    media_url   VARCHAR(500),
    created_at  TIMESTAMP DEFAULT NOW()
);

-- The social graph: who follows whom
CREATE TABLE follows (
    follower_id BIGINT,
    followee_id BIGINT,
    created_at  TIMESTAMP DEFAULT NOW(),
    PRIMARY KEY (follower_id, followee_id)
);

The follows table is the heart of the system. The query “who follows user X?” (for fan-out) and “who does user Y follow?” (for pulling) both run against it, so it is heavily indexed and usually sharded by both columns.

API Design

POST   /api/v1/posts
  body: { content, media_url? }
  response: { post_id, created_at }

GET    /api/v1/feed?cursor={opaque}&limit=20
  response: { posts: [ {post_id, author, content, media_url, created_at} ],
              next_cursor }

POST   /api/v1/users/{id}/follow
DELETE /api/v1/users/{id}/follow

Note the cursor-based pagination. We never use OFFSET — on an infinite, constantly-growing feed, offset pagination skips or repeats posts as new ones arrive. The cursor encodes “the last post ID you saw,” and we fetch posts older than it.

High-Level Design

News feed architecture showing the write path from user posts through the Post Service and an asynchronous Fan-out Service that reads the follow graph and pushes post IDs into per-user Feed Caches, and the read path where the Feed Service returns the precomputed feed

The design splits cleanly into a write path and a read path, and the whole art of this problem is deciding how much work each one does.

Post Service accepts a new post, assigns it a time-sortable ID (a Snowflake-style ID so posts sort by creation time), writes it to the posts store, and emits a post.created event.

Fan-out Service is a pool of asynchronous workers that consume post.created events. For each new post, it looks up the author’s followers in the social graph and pushes the post ID into each follower’s feed cache.

Feed Cache holds, per user, a list of the most recent ~500–1000 post IDs they should see — typically a Redis sorted set or list keyed by user ID. This is the precomputed feed.

Feed Service serves reads. On GET /feed, it reads the user’s precomputed list of post IDs from the feed cache, hydrates them (fetches the actual post content and author info, usually from a separate cache), and returns the page. Because the heavy lifting already happened at write time, a read is just “fetch a list, hydrate, return.”

Deep Dive: Fan-out on Write vs Fan-out on Read

This is the crux of the entire design, and the interviewer will spend most of their time here.

Comparison of fan-out on write (post once, write into N follower feeds, fast reads but expensive for celebrities) versus fan-out on read (pull and merge followees at read time, cheap writes but slow reads), and the hybrid that combines both

Fan-out on write (push model)

When a user posts, immediately push the post ID into every follower’s feed cache. Reads are then trivially fast — the feed is already assembled.

  • Pro: Reads are O(1) — just read a precomputed list. Perfect for our 100:1 read-heavy workload.
  • Con: A post by someone with 100 million followers triggers 100 million cache writes. That is the celebrity (hot-user) problem, and it will melt your fan-out tier and delay the feed for everyone.

Fan-out on read (pull model)

Store nothing precomputed. When a user opens their feed, fetch the recent posts of everyone they follow and merge them on the fly.

  • Pro: Writes are cheap — a post is one row. No celebrity blow-up.
  • Con: Reads are expensive — merging the latest posts from 200+ followees on every single feed load, for millions of concurrent users, is far too slow for a 200 ms budget.

The hybrid (what real systems do)

Neither pure approach survives at scale, so production systems combine them:

  • Normal users → fan-out on write. Precompute feeds for the vast majority of accounts. Reads stay fast.
  • Celebrities → fan-out on read. For a small set of accounts with huge follower counts, do not fan their posts out. Instead, at read time, pull their recent posts and merge them into the user’s precomputed feed.
def get_feed(user_id, cursor, limit=20):
    # 1. The precomputed part (from fan-out on write)
    feed_ids = feed_cache.range(user_id, cursor, limit)

    # 2. The pull part: celebrities this user follows
    celeb_ids = graph.celebrity_followees(user_id)   # small set
    for celeb in celeb_ids:
        feed_ids += post_cache.recent(celeb, since=cursor)

    # 3. Merge, de-dup, sort by (time-sortable) id, take the page
    merged = sorted(set(feed_ids), reverse=True)[:limit]
    return hydrate(merged)

The threshold for “celebrity” (say, >1M followers) is tunable. This hybrid keeps writes bounded (you never fan out to millions) while keeping the common read path fast.

Deep Dive: Feed Storage, Hydration, and Ranking

Storing the feed

Each user’s feed cache entry is a capped list of post IDs only — not full post content. Storing IDs keeps the per-user footprint tiny (a few KB) and avoids duplicating post bodies across millions of feeds. A Redis sorted set works well, scored by the post’s time-sortable ID:

ZADD feed:{user_id} {post_id} {post_id}      # score == id, so sorted by time
ZREMRANGEBYRANK feed:{user_id} 0 -1001       # cap at the newest 1000

Hydration

At read time, the Feed Service takes the page of post IDs and fetches the actual content from a post cache (another Redis tier, keyed by post ID, backed by the posts DB). One MGET of 20 post IDs returns the content; a parallel lookup fills in author handles and avatars. Hydrating on read (rather than storing full posts in every feed) means an edited or deleted post is reflected everywhere automatically.

Ranking: chronological vs scored

The simplest feed is reverse-chronological — sort by post ID descending. It is predictable and cheap, and a great default answer.

Modern feeds (Instagram, Facebook) use a ranked feed: a scoring model orders candidate posts by predicted engagement (recency, affinity to the author, media type, past interactions). The architecture barely changes — you still gather candidate post IDs via the same fan-out, but a ranking service scores and re-orders them before returning the page. Mention this as an extension; do not over-build it in the interview.

Deep Dive: Freshness, Consistency, and Pagination

Consistency. A feed does not need strong consistency. If a post takes two or three seconds to appear because the fan-out workers are catching up, no one notices or cares. Embracing eventual consistency here is what lets the write path be asynchronous and horizontally scalable.

Freshness. New posts appear as fan-out workers drain the post.created stream. During a traffic spike the queue absorbs the burst and workers catch up — the feed lags by seconds, never breaks. For celebrity posts, freshness comes for free because they are pulled live at read time.

Pagination. Cursor-based paging is essential:

GET /feed?limit=20            → returns 20 posts + next_cursor = last_post_id
GET /feed?cursor=<id>&limit=20 → returns 20 posts older than <id>

Because post IDs are time-sortable, the cursor is just the smallest ID in the current page, and the next query asks for IDs below it. New posts arriving at the top never disturb the page boundaries below, so the user never sees duplicates or gaps while scrolling.

Key Takeaways

Decision Choice Why
Feed assembly Hybrid fan-out Push for normal users (fast reads), pull for celebrities (bounded writes)
Feed cache Redis sorted set of post IDs Tiny per-user footprint, time-sorted, capped
Post content Hydrated at read from a post cache No duplication; edits/deletes reflect everywhere
Fan-out Async worker pool off a queue Absorbs spikes, keeps posting fast
Post IDs Time-sortable (Snowflake) Enables cursor pagination and cheap sorting
Pagination Cursor-based No skips/dupes on a growing feed
Consistency Eventual Seconds of feed lag is invisible and unlocks async scale

The news feed is the canonical read-heavy design. The interviewer wants to see you (1) identify the 100:1 read:write ratio, (2) reach for fan-out on write to make reads cheap, (3) recognize the celebrity problem it creates, and (4) resolve it with a hybrid. If you drive that arc yourself, you have demonstrated exactly the judgment the question is testing.