Infinite scroll: the client keeps asking for the next page
Non-functional
Target
Meaning
Latency
feed load p99 < 300 ms
99% of opens are faster than this
Availability
99.99%
about 52 min of downtime a year
Consistency
eventual
new post visible within ~5 s is fine
3. Scale Estimates
System Design Case StudiesPage 2
High-Level Architecture
1. The Eight Components
Component
Job
API gateway
Auth, rate limit, routes each call to a service
Post Service
Validates a post, stores metadata, emits PostCreated
Graph Service
Who follows whom: followers(u) and following(u)
Fan-out Service
Workers that push post_id into followers' feeds
System Design Case StudiesPage 3
Post Service: Publishing a Post
1. Responsibility and Upload Flow
Running example: Instagram Feed. Post Service owns metadata only; media bytes go straight to S3.
Validate the request: caption length, media_id exists and is owned by the author
Store post metadata in the posts table
Emit a PostCreated event to Kafka for fan-out
Media is uploaded separately, never through Post Service
Pre-signed URLclient asks Media Service
PUT to S3direct upload
POST /v1/postswith media_id
Transcodeasync workers
PUBLISHEDvisible in feeds
System Design Case StudiesPage 4
Follow Graph Service
1. Responsibility and Data Model
Stores who follows whom for the Instagram feed
Answers followers(user) and following(user) fast
Feeds the Fan-out Service (who to push to) and Feed Service (who to pull from)
Estimate: 500M users x 200 edges = ~100B rows, ~16 B each, ~1.6 TB per table
Table
Columns
Shard key
Answers
System Design Case StudiesPage 5
Fan-out on Write vs Fan-out on Read
(Instagram Feed) Two ways to build a home feed: do the work when a post is written, or when the feed is opened.
1. Fan-out on Write (Push)
At post time, insert post_id into every follower's feed list
Feed list lives in the Redis feed cache
Read = one cache lookup, O(1) in the number of followed accounts
Write = O(followers) inserts per post
Post createdPostCreated event
Graph lookupfollowers(author)
Fan-out workers1 insert per follower
Feed cachefeed:{follower}
System Design Case StudiesPage 6
The Celebrity Problem and Hybrid Fan-out
1. The Problem
Instagram Feed: pure push fan-out breaks on one account with a huge follower count.
Account with 100M followers posts once
Push writes 100M feed inserts for that one post
At ~230K inserts/s that takes minutes, not seconds
Writes pile onto the same shards: hot shards
Followers see the post at very different times
2. Hybrid Fan-out
System Design Case StudiesPage 7
Feed Cache and Feed Generation
Instagram News Feed: the home feed is read from a Redis cache of ids, never assembled from the database on the hot path.
1. What the Cache Holds
Feed Service serves the home feed from Redis
Stores post_ids only, not full posts
Metadata and counters are fetched at read time, so edits and likes are never stale in the cache
Key: sorted set feed:{user_id}
Member = post_id (8 bytes)
System Design Case StudiesPage 8
Feed Ranking
1. What Ranking Does
Order candidates by predicted engagement, not pure time
Input: ~500 candidate post_ids from the feed cache plus the celebrity merge
Output: the top page of 20 posts for GET /v1/feed
2. Pipeline and Features
Candidates~500 posts
Light scoringcheap model, cut to 100
Heavy ML modeltop ~100 only
Diversity re-rankmix authors and media
System Design Case StudiesPage 9
Media Storage, Processing and CDN
Running example: Instagram Feed. Media is 200 TB/day (100M posts x 2 MB), so bytes never touch app servers.
1. Upload and Processing
Pre-signed URLfrom Media Service
PUT to S3direct from client
Upload eventKafka
Transcode workersasync renditions
Media READYpost can PUBLISH
Client asks for a pre-signed URL, then PUTs straight to the object store (S3)
App servers are bypassed: no upload bandwidth or memory on the API tier
System Design Case StudiesPage 10
Pagination, Likes and Comments Counters
1. Cursor Pagination
Offset pagination breaks on feeds: a new post shifts every item down
Page 2 then repeats items already seen on page 1: duplicates
Use a cursor: an opaque token that encodes the last (score, post_id)
Score = millisecond timestamp; same-millisecond ties are broken by post_id
Client sends the token back: GET /v1/feed?cursor=<token>&limit=20
redis
# page 1
ZREVRANGEBYSCORE feed:42 +inf -inf WITHSCORES LIMIT 020# next page: score taken from the cursor
ZREVRANGEBYSCORE feed:421730100000123 -inf WITHSCORES LIMIT 020
Inclusive score; drop ids at that score already seen
System Design Case StudiesPage 11
Scaling, Failure Handling, Trade-offs and Interview Questions
Running example: Instagram News Feed at 500M DAU, ~170K peak read QPS, feed p99 under 300 ms.
1. Scaling the feed
Data
Shard key
Why
Posts
author_id
one author's posts live on one shard
Feeds (Redis)
user_id
one feed read hits one shard, ~3.2 TB total
System Design Case StudiesPage 12
News Feed Cheatsheet
1. Numbers (Instagram News Feed)
Metric
Estimate
Daily active users
500M DAU
Feed reads
5B opens/day = ~58K QPS avg, 170K peak
Post writes
100M/day = ~1,150/s avg (3,500 peak)
Read:write
~50:1
Media
100M x 2 MB = 200 TB/day
System Design Case StudiesPage 13
Check yourself
A user has read page 1 (items 1-20) of their feed. Five new posts arrive, then the client asks for page 2 with offset=20. What does the user see, and what is the fix?
The five new posts push every older item down by 5 positions
Offset 20 now starts at what used to be item 16, so items 16-20 appear a second time
Fix: cursor pagination, where the cursor is the last (score, post_id) seen
The query becomes ZREVRANGEBYSCORE below the cursor, so new posts at the top cannot shift it
A team sets the celebrity threshold to 10K followers, so every account above it is pull-based. Writes get cheaper. What breaks?
Many more accounts become pull-based, so each feed read must fetch and merge many more celebrity timelines
500M DAU, 100M new posts/day
Writes: 100M / 86,400 s = ~1,150 posts/s avg, ~3,500/s peak
Feed reads
1500M DAUdaily users
2x 10 opensper user per day
35B opens/daytotal reads
4/ 86,400 s~58K QPS avg
5Peak~170K QPS
Quantity
Math
Result
Post writes
100M / day
~1,150/s avg, ~3,500/s peak
Feed reads
500M x 10 = 5B / day
~58K QPS avg, ~170K peak
Read:write
58K / 1,150
~50:1 (read-heavy)
Media storage
100M x 2 MB
200 TB/day, ~73 PB/year
Metadata
100M x ~1 KB
100 GB/day
Remember
Reads are ~50x writes, so optimize the read path. Media (200 TB/day) dwarfs metadata (100 GB/day): store them separately.
Feed Service
Builds the home feed: cache, hydrate, merge
Ranking Service
Orders candidates by predicted engagement
Media Service
Pre-signed uploads, transcode, CDN URLs
Counter Service
Likes and comment counts via Redis INCR
2. Write Path: Post Then Fan Out
Write path (~1,150 posts/s avg, ~3,500/s peak)
1ClientPOST /v1/posts
2Post Servicevalidate, save
3DB + KafkaPostCreated event
4Fan-out workersread followers
5Feed cacheRedis ZSET insert
Post Service returns after DB write + event, not after fan-out
Fan-out is async: ~200 followers x 1,150/s = ~230K feed inserts/s
New post visible in feeds within ~5 s (eventual consistency)
3. Read Path: Cache, Hydrate, Rank
ClientGET /v1/feed
Feed Serviceentry point
Redis feedpost_ids only
Hydrateposts + counters
RankRanking Service
Response20 posts
Reads (~58K QPS avg, ~170K peak) touch only the precomputed cache
Reads never block on fan-out; a half-done fan-out just means a slightly stale feed
Target: feed p99 under 300 ms
4. Where Data Lives
Store
Holds
Sharded by
MySQL / Cassandra
Post metadata (~1 KB each)
author_id
Graph store
Follow edges, both directions
user_id
Redis
Feeds (ZSET of post_ids), counters
user_id
S3 + CDN
Photo/video blobs (~200 TB/day)
object key
Async is the design
Kafka decouples posting from fan-out: post latency stays low, and read latency never depends on write load.
Offset paging duplicates when new posts shift items
Likes: INCR in Redis, flush batched to DB about every 1 s via Kafka
Hot post: split into 16 sub-counters, sum on read
Counts are approximate; reconcile nightly from the likes table
Remember
Push for normal users, pull for celebrities, cache only post_ids, and never make a read wait on fan-out.
That work lands on the read path, which already runs at ~170K QPS peak with a p99 budget of 300 ms
Feed latency rises and the ranking budget shrinks
Set the threshold from measured write cost (e.g. ~1M followers) and re-evaluate it periodically
A user unfollows an account, but its old posts still show up in their feed. Why, and how should it be handled?
The feed ZSET holds post_ids that were pushed in earlier, and unfollowing does not remove them
Eagerly scanning and deleting from the cached list is expensive and racy
Handle it lazily: at read time, hydrate the posts and drop any whose author is no longer followed
The old entries then age out as the list hits its 800-entry cap or its TTL
The Post Service commits the post row to the DB, then crashes before publishing PostCreated to Kafka. What does the author see, and what prevents it?
The post exists in the DB, so the author can open it
No fan-out workers ever get the event, so no follower's feed gets the post
Fix: transactional outbox. Write the post and an outbox row in the same DB transaction
A relay publishes outbox rows to Kafka and retries until it succeeds
Fan-out must be idempotent, because the relay can deliver an event more than once
A viral post gets ~100K like events/s through a single Redis INCR key. Likes are also retried by clients. What goes wrong, and what is the fix for each problem?
One key lives on one Redis shard, so that shard becomes a hot spot
Fix: split the counter into N sub-counters (e.g. 16), INCR a random one, and sum all 16 on read
A client retry would count the same like twice
Fix: insert into likes(post_id, user_id) with a unique key, and INCR only if the insert is new
The counts are approximate, so reconcile them from the likes table nightly