Case Study 4: Instagram / Twitter News Feed
Show the posts of the people a user follows in a fast, personalised home timeline, in milliseconds, for hundreds of millions of users.
C.1 Requirements
Functional requirements:
- A user can create a post (with text, photo, video).
- A user can follow / unfollow other users.
- Home feed: the latest posts of followed users, ranked or in reverse-chronological order.
- Users can like, comment and share; every post shows its like count.
- The feed is an infinite scroll: cursor-based pagination.
- User profile timeline: all posts of one user.
- Notification of a new post to followers (optional, async).
Non-functional requirements:
- High availability 99.99%: a slightly stale feed is fine, but the feed must not go down (AP over CP).
- Feed load latency p99 < 200 ms, post publish < 500 ms.
- Eventual consistency is OK: it is fine if a new post shows up for followers within 5-10 seconds.
- Scale: 50 crore (500 million) DAU, a read-heavy system (read:write about 100:1).
- The like counter can be approximately correct, but never negative or badly wrong.
- Durable media storage (11 nines): an S3-like object store + CDN.
C.2 Estimation (back-of-the-envelope)
| Item | Number | How it is derived |
|---|---|---|
| Daily active users | 50 crore | Assumption: a global app at Instagram/Twitter scale. |
| Posts per day | 10 crore | One in every 5 users posts once a day; 10 crore / 86400 = ~1,200 writes/sec avg, peak ~5,000/sec. |
| Feed read QPS | ~1.2 lakh/sec avg | 50 crore users x 20 feed refreshes/day = 1000 crore / 86400 = ~1.15 lakh/sec; peak 3x = ~3.5 lakh/sec. |
| Fan-out cache writes | ~10 lakh/sec peak | Avg 200 followers x 1,200 posts/sec = 2.4 lakh/sec avg; peak 5,000 x 200 = 10 lakh/sec; pushing celebrity posts would make this explode, hence hybrid. |
| Post metadata storage | ~1 TB/day | 10 crore posts x ~1 KB metadata = 100 GB/day; with replication x3 + indexes ~0.5-1 TB/day, ~300 TB a year. |
| Media storage | ~20 PB/year | Half of the posts carry a photo, 1 MB avg (multiple resolutions) = 50 TB/day x 365 = ~18 PB/year. |
| Timeline cache size | ~4 TB RAM | 20 crore active users x 500 post IDs x 8 bytes = ~800 GB; with overhead and replicas, a ~3-4 TB Redis cluster. |
| Like events | ~10 lakh/sec peak | There are some likes on every feed view; the counters must be handled with Redis + batch flush. |
C.3 API design
| Endpoint | Purpose |
|---|---|
| POST /v1/posts | Create a new post (caption, media_ids); with an Idempotency-Key header so no double post is created |
| POST /v1/media/upload-url | Get an S3 pre-signed URL; the client uploads the media directly |
| GET /v1/feed?cursor=<id>&limit=20 | Next page of the home timeline; cursor-based pagination |
| GET /v1/users/{id}/posts?cursor= | A user's profile timeline |
| POST /v1/users/{id}/follow | Follow; DELETE on the same path to unfollow |
| POST /v1/posts/{id}/like | Like (idempotent: liking again does not raise the count); DELETE to unlike |
C.4 Data model
| Table / Store | Fields | Why |
|---|---|---|
| users (MySQL/Postgres, sharded by user_id) | user_id PK, username, name, follower_count, is_celebrity, created_at | The user profile is relational and rarely changes; the is_celebrity flag decides fan-out. |
| posts (Cassandra, partition key author_id, clustering post_id DESC) | post_id (Snowflake ID), author_id, caption, media_urls, created_at | Write-heavy, time-ordered data; one author's posts sit in one partition, latest first. |
| follows (Cassandra, do tables) | followers(user_id, follower_id) aur following(user_id, followee_id) | Fast lookup is needed in both directions: followers for fan-out, following for pull. |
| timeline cache (Redis sorted set / list) | key feed:{user_id} -> latest 500 post_ids with score = timestamp | A precomputed home feed with O(log n) reads in memory; only IDs are stored and the data is hydrated separately. |
| post_counters (Redis + Cassandra counter table) | post_id, like_count, comment_count | Hot counters are INCR'd in Redis and flushed to the durable store periodically. |
| likes (Cassandra, partition key post_id) | post_id, user_id, created_at | Record of who liked; because (post_id, user_id) is unique the like stays idempotent. |
C.5 High-level architecture
- 1.Clients: Mobile App, Web Browser; media loads from CDN (origin S3 media: photos, resized)
- 2.Load Balancer -> API Gateway (auth, rate limit)
- 3.Services: Post Service (Snowflake post_id), Feed Service (merge + hydrate), Follow Service (celebrity flag), Like Service (idempotent like), Media Service (upload-url, resize), Ranking (ML score)
- 4.Kafka: post-created and like events
- 5.Workers: Fan-out Workers (paginate followers, skip celebrities), Counter Aggregator (batch flush every 5-10 sec), Ranking Service
- 6.Data: Redis Timeline feed:{user_id} (500 IDs, ~4 TB), Cassandra (posts by author_id + post_id DESC, follows, likes, counter table), MySQL (users, is_celebrity), Redis Counters (INCR likes:{id}), S3 (media)
Post publish: S3 upload -> Gateway -> Post Service -> Cassandra -> Kafka post-created -> ZADD into followers' timelines. Feed read: Redis feed IDs + celebs pull from Cassandra -> Ranking -> hydrate post cache -> CDN URLs. Numbers: feed 1.2 lakh/sec avg, 3.5 lakh peak; 1,200 posts/sec; fan-out peak 10 lakh writes/sec. Hybrid fan-out: normal user push on write, celebrity (>10 lakh followers) pull on read.
- The photo is first uploaded straight to S3 through a pre-signed URL from the Media Service; images load from the CDN.
- Post publish: LB -> API Gateway -> Post Service writes to Cassandra with a Snowflake ID, and an event goes on Kafka post-created.
- Fan-out Workers paginate the followers and ZADD the post_id into every active follower's Redis timeline feed:{id}; celebrities are skipped.
- Feed read: Feed Service merges the Redis IDs + celebrities' posts from Cassandra and gets them scored by the Ranking Service.
- Like Service INCRs the Redis Counters; the like event goes through Kafka and the Counter Aggregator flushes to Cassandra in batches.
| Component | What it does |
|---|---|
| Post Service | Validates the post, saves it in Cassandra, assigns a Snowflake ID, then publishes a post-created event to Kafka. |
| Media Service | Gives a pre-signed S3 URL; after upload async workers create thumbnails/resizes, served through the CDN. |
| Fan-out Workers | Read the event from Kafka, fetch the author's followers and push the post_id into every follower's Redis timeline (for normal users). |
| Feed Service | Merges precomputed IDs from Redis + the latest posts of followed celebrities, applies ranking, hydrates the post data and returns it. |
| Follow Service | Maintains the follow graph; updates follower_count and sets the flag when the celebrity threshold is crossed. |
| Like Service + Counter Aggregator | The like record goes to Cassandra, the count via Redis INCR; the aggregator flushes to the durable store in batches every few seconds. |
| Ranking Service | Scores the feed with an ML model (engagement, recency, closeness); Instagram publicly uses a ranked feed. |
C.6 Main flows
Post publish and fan-out on write:
- The client first gets a pre-signed URL from /media/upload-url and uploads the photo directly to S3.
- The client sends POST /v1/posts with an Idempotency-Key.
- The API Gateway checks auth and rate limit; the request reaches Post Service.
- Post Service creates a Snowflake post_id, writes to the Cassandra posts table and returns 201 to the client.
- Post Service puts an event on the Kafka topic post-created (author_id, post_id, ts).
- The Fan-out Worker reads the event; if the author is a celebrity it skips (pull model), otherwise it paginates the followers list.
- The post_id is ZADDed into every active follower's Redis feed:{id} sorted set, trimmed at size 500.
- Inactive followers (no login for 30 days) are skipped; their feed is rebuilt at login.
Home feed read (hybrid merge):
- The client sends GET /v1/feed?cursor=.
- Feed Service reads the top N post_ids of feed:{user_id} from Redis (after the cursor).
- It picks the celebrities from the following list and pulls their latest posts from Cassandra/cache.
- The two lists are merged and the Ranking Service scores them.
- The post IDs are hydrated with a multi-get from the post cache (Redis); Cassandra on a miss.
- Like counts from the Redis counters and the CDN media URLs are attached.
- The response is returned with next_cursor; the client loads images from the CDN.
Liking and counter update:
- The client sends POST /v1/posts/{id}/like.
- Like Service inserts (post_id, user_id) into the Cassandra likes table; if it already exists it is a no-op (idempotent).
- For a new like, INCR likes:{post_id} in Redis.
- The like event goes to Kafka for notifications and analytics.
- Every 5-10 seconds the Counter Aggregator persists the Redis counts in batches to the Cassandra counter table.
- The client sees the updated count instantly through an optimistic UI.
C.7 Deep dives
- 1.Normal user (avg 200 followers): PUSH. Post -> Kafka post-created -> Fan-out Worker (followers list)
- 2.ZADD post_id into feed:{f1}, feed:{f2}, feed:{f3}, trim at size 500; skip inactive f4 (30 days inactive = skip, rebuilt at login)
- 3.Celebrity (10 crore followers): PULL. Pushing would be 1 post = 10 crore Redis writes = minutes; so skip fan-out, the post stays only in the Cassandra author partition
- 4.The follower opens the feed (GET /v1/feed): at read time the celeb's latest posts are pulled from Cassandra + merged; few celebs are followed
Push: fast reads (precomputed) but expensive writes, wasted on inactive users. Hybrid = push for most, pull for celebrities; the Follow Service sets the is_celebrity flag when the threshold is crossed.
- 1.Redis feed:{uid}: top N IDs after the cursor; celebs' latest posts: following list -> Cassandra / cache
- 2.Merge both lists -> Ranking Service score
- 3.Hydrate: Redis post cache multi-get; miss -> Cassandra; drop deleted / unfollowed posts
- 4.Attach like counts (Redis) + media CDN URLs
- 5.Response: 20 posts + next_cursor (Snowflake ID = time order)
- 1.POST /posts/{id}/like -> Cassandra likes (post_id, user_id unique; repeat = no-op)
- 2.Redis INCR likes:{post_id} on a new like
- 3.Hot post? Shard into N sub-keys, sum on read
- 4.Kafka like event -> notification, analytics
- 5.Aggregator every 5-10 sec -> Cassandra counter
Problem: the celebrity problem (an account with 10 crore followers). With fan-out on write one post = 10 crore Redis writes, taking minutes. So hybrid: push for normal users, pull at read time and merge for celebrities (for example >10 lakh followers).
Problem: fan-out on write vs fan-out on read. The push model has fast reads (precomputed) but expensive writes, wasted on inactive users. The pull model has cheap writes but queries N followees on every read. Hybrid is best: push for most, pull for celebrities.
Problem: the like counter of a hot post (lakhs of likes per minute). To avoid contention on one DB row use Redis INCR; for very hot keys shard the counter into N sub-keys and sum on read. Flush in batches to the durable store.
Problem: the timeline cache grows very large. Store only post IDs (8 bytes), not the whole post. Trim every feed at 500 entries, keep only active users' feeds in the cache; rebuild inactive ones from Cassandra at login.
Problem: globally unique, time-sortable post IDs. Snowflake-style 64-bit ID: timestamp + machine id + sequence. Unique without a central DB, and the ID itself gives time ordering, which is useful for cursor pagination.
Problem: a post still shows in the feed after an unfollow or post delete. Filter at read time: while hydrating, drop deleted posts and unfollowed authors. A background job cleans up the timelines; eventual consistency.
C.8 Trade-offs
| Decision | What was chosen and why |
|---|---|
| Push vs Pull vs Hybrid fan-out | Chose hybrid: fast precomputed reads for 99% of users, and avoids the write explosion for celebrities. |
| Cassandra vs MySQL for posts | Cassandra: write-heavy, a time-series-like access pattern, easy horizontal scaling; no joins needed. |
| Exact vs approximate like count | Approximate real-time (Redis) + eventually exact (batch flush); a user does not care about 1,23,456 vs 1,23,460. |
| Ranked vs chronological feed | A ranked feed raises engagement but needs ML infra and adds complexity; an MVP can start with chronological. |
| Strong vs eventual consistency | Eventual: availability and latency matter more; it is fine if a post shows up 5 seconds late. |
C.9 Interview tips
- State the read:write ratio first: it is read-heavy, which makes the case for precomputing (fan-out on write).
- Raise the celebrity problem yourself and explain the hybrid model; this is what the interviewer wants to hear.
- Show why the timeline cache holds only IDs, and the memory math.
- Mention hot-key sharding and batch flush for the like counter.
- Do not forget to handle media on a separate path (pre-signed S3 + CDN).