Designing Instagram is really one hard question wearing a costume: how do you build the feed? Photo upload and following are warm-ups; the interview is decided by how you generate each user's home feed at scale — and whether you can reason about the fan-out trade-off with real numbers. This walkthrough applies the standard system design framework to Instagram, with timings for a 45-minute round.
1. Requirements
Functional: upload a photo (with caption), follow/unfollow users, view a home feed of recent posts from people you follow, like/comment. Out of scope (say so explicitly to manage time): stories, DMs, explore/recommendations, ads.
Non-functional, quantified: read-heavy (people scroll far more than they post — about 20:1 with the assumptions below, higher in practice), highly available, feed latency under 200 ms, durable media (photos must never be lost), and eventual consistency is fine (a new post taking a few seconds to appear in followers' feeds is acceptable).
2. Back-of-the-envelope
Assume 500M daily active users, each posting ~once/2 days and reading the feed ~10×/day. Don't stop at QPS — run the estimate all the way to a conclusion about where the bytes go.
- Writes (posts): ~250M/day ≈ ~3,000/sec. Reads (feed loads): ~5B/day ≈ ~58,000/sec → reads outnumber writes ~20:1, so the read path dominates.
- Media: ~250M posts/day × ~2 MB ≈ ~500 TB/day → photos live in object storage (S3) + CDN, never in the database. The DB stores metadata and pointers, not bytes.
- Metadata sizing. A post row —
postId,userId, a media URL, a caption, a timestamp, a couple of counters — is only ~0.5 KB. So250M/day × 0.5 KB ≈ 125 GB/day, ~230 TB over five years, and the follow graph (hundreds of billions of edges at ~16 bytes — 500M users × a few hundred follows) is only a few TB. Conclusion: the metadata is trivial next to the media — it's the photo bytes and the read QPS that you design around, not the database size. - Bandwidth. Ingress is uploads —
3,000/sec × 2 MB ≈ ~6 GB/s— but it goes straight to S3 via presigned URLs, bypassing your app tier. Egress is the punchline: a feed page pulls ~10 images at a feed-sized ~200 KB variant, so58,000 feed-loads/sec × ~2 MB ≈ ~115 GB/sof image egress. No origin serves that — it's why the CDN is non-negotiable, and why your app servers stay bandwidth-light. - Feed cache + the 80/20 rule. Storing ~500 post ids per user at ~16 bytes across 500M users would be ~4 TB of Redis if every feed were kept warm — but inactive users are regenerated lazily (Section 7), so the live footprint stays well under that. You don't cache every post body — a power law means the hot ~20% of posts serve ~80% of reads, so a post-body cache sized to that working set (tens of GB) gives a 90%+ hit rate. Size both fleets to ~70% utilization so a spike has headroom — that's the number that tells the interviewer you've actually costed it.
3. API
POST /media/upload-url { contentType } -> { uploadUrl, mediaUrl } // presigned S3 URL
POST /posts { mediaUrl, caption } -> { postId }
GET /feed?cursor=... -> { posts[], nextCursor }
POST /follow { userId }
The client uploads the image directly to S3 via the presigned uploadUrl (so the photo bytes never pass through your app servers), then creates the post with the resulting mediaUrl. Every endpoint sits behind the authenticated user's session/JWT — the userId comes from the token, never the request body. Feed pagination is cursor-based, not offset-based — offsets break when new posts shift positions.
4. Data model
Access-pattern first: users; posts (postId, userId, mediaUrl, caption, createdAt); follows (followerId, followeeId); and a per-user feed cache. Posts and follows are simple lookups and time-ordered reads — a wide-column store (Cassandra) or sharded SQL both work. The interesting structure is the feed cache (Section 7); how the store is partitioned and how postId is generated are their own decisions (Section 8).
Here is the system you're building:
5. The crux: generating the feed (fan-out)
This is where the interview is won. Two strategies:
| Strategy | How | Pros | Cons |
|---|---|---|---|
| Fan-out on write (push) | When you post, write the post id into every follower's precomputed feed list | Feed reads are O(1) and instant | A celebrity post writes to millions of feeds — write amplification |
| Fan-out on read (pull) | Build the feed at read time by querying recent posts from everyone you follow | Cheap writes; no wasted work for inactive users | Expensive, slow reads — especially if you follow many people |
The senior answer is a hybrid. Use push for ordinary users (precompute followers' feeds via an async worker, so reads are instant), but for celebrities switch to pull — their posts are fetched and merged into the feed at read time. You avoid both the write storm and slow reads. Drag the slider to feel exactly where push stops scaling:
Fan-out simulator
interactiveWith 805 followers we push: each post writes to 805 feeds, so reads are instant. Cheap and worth it below the threshold.
The threshold is not arbitrary — it falls out of the math. A normal user with a few hundred followers costs a few hundred feed writes per post: trivial. A celebrity with 50 million followers costs 50 million writes per post, and posting ~10 times a day is half a billion feed writes a day for one account. That is the write amplification that makes pure push impossible, and the reason you flip to pull somewhere around 10k–100k followers. Naming that number, and the switch, is what separates a strong candidate.
6. The fan-out pipeline
Fan-out must be asynchronous — you cannot make the poster wait while millions of feeds update. Here is the write path and the read path:
- Upload: client uploads the image to S3 via a presigned URL → creates the post (metadata to the DB) → the post service enqueues a fan-out job on a message queue.
- Fan-out worker: reads the poster's followers and
ZADDs the post id into each follower's feed cache — skipping celebrity accounts (their posts are pulled, not pushed). - Read feed: load the precomputed feed (
ZREVRANGE), merge in recent posts from any celebrities you follow (pulled and sorted by time), hydrate post metadata from a post cache, and return image URLs that point at the CDN.
7. The feed cache, concretely
The feed cache is a per-user Redis sorted set: the member is a postId and the score is the post's timestamp.
ZADD feed:{userId} {timestamp} {postId} # fan-out worker writes a new post
ZREVRANGE feed:{userId} 0 19 # read the latest 20 for the feed page
Three details that signal depth:
- Store ids, not posts. The set holds only post ids; the post bodies (caption, media URL, like count) are hydrated from a post cache at read time, so one edited or deleted post doesn't require rewriting millions of feeds.
- Cap the length. Keep only the most recent ~500 ids per user (
ZREMRANGEBYRANK), since no one scrolls further; this bounds memory. - Regenerate lazily. Don't keep feeds warm for users who haven't opened the app in weeks — rebuild their feed on next read from the posts of who they follow.
8. Partitioning the metadata & generating post ids
The fan-out and feed cache scale the read path. The source-of-truth metadata (posts, follows) still has to shard, and how you pick the shard key is a senior probe in its own right.
Shard by user, not by post. Two candidates:
| Shard key | What it's good at | What breaks |
|---|---|---|
By userId | "All of a user's posts" and "a user's follow list" are single-shard reads | Hot users (a celebrity's shard runs hot), uneven growth (power users have 1000× more posts), blast radius (that shard down = all their content gone) |
By postId | Writes spread perfectly evenly; no hot-user shard | "A user's posts" becomes a scatter-gather across every shard, and it creates a chicken-and-egg: if the id picks the shard, you can't mint it with a per-shard counter |
The rule that resolves it: shard by the entity you range-scan together. We almost always read a user's posts and a user's followers as a unit, so partition by userId — and the hot-user problem is the same problem the celebrity pull path already solves, with a per-user post cache absorbing the rest. Use consistent hashing so adding a shard remaps only a slice and a dead shard's range is reassigned without a full reshard, and run many logical partitions over fewer physical nodes with a partition→node map you edit to migrate load — elastic resharding that never touches the keys.
Generating post ids (the chicken-and-egg, solved). A central auto-increment is a single write bottleneck; per-shard counters collide across shards. Use a Snowflake-style, time-sortable id — roughly timestamp-ms | worker-id | per-ms sequence — minted locally with zero coordination. The bonus is the whole reason it's worth doing here: because the id embeds time, posts sort by recency straight off the primary key, so the feed's "newest first" ordering and pagination come for free with no separate time index. It's the same id-generation move as the counter in the URL shortener — steal it wholesale.
Split reads from writes. An upload is a heavy, connection-holding write; a feed load is light and cache-first; and reads outnumber writes 20:1. So split the stateless tier into a write service (create post → enqueue fan-out) and a read service (serve feeds, cache-first) so an upload burst can't starve readers, and scale far more read instances than write.
9. Media delivery
Photos are the bulk of the bytes, so the media path is its own mini-design: the client uploads directly to S3 via a presigned URL; an ingest step generates resized variants (thumbnail, feed, full) so each surface fetches the right size; and everything is served through a CDN so images come from an edge near the user. The database never holds image bytes — only the URL.
10. Durability, availability & multi-region
- Media durability comes from S3's cross-region replication; losing a photo is unacceptable, losing a feed-cache entry is not (it can be rebuilt).
- Metadata (posts, follows) is replicated; reads can come from replicas, accepting replication lag.
- The feed cache is rebuildable — if a Redis shard is lost, regenerate those users' feeds from the posts store, so it can run with weaker durability than the source of truth.
- Multi-region: route users to a home region with regional feed caches and CDN edges; cross-region follows are reconciled asynchronously.
11. Failure modes interviewers probe
Fan-out worker backlog
A burst of posts (or a slow Redis) backs up the fan-out queue, so new posts appear late in feeds. Scale workers horizontally off the queue depth, and prioritize — a backlog delays propagation, but because the write is durable in the posts store first, nothing is lost.
Read-your-writes
After you post, you expect to see it immediately, but async fan-out hasn't reached your own feed yet. Write the author's post into their own feed synchronously on publish, so the poster always sees their post even while fan-out to followers catches up.
Feed-cache loss / cold start
If a feed-cache shard dies, those users hit a cold cache and the read path falls back to rebuilding from the posts store — a temporary latency spike. Rebuild lazily and rate-limit the regeneration so the posts store isn't stampeded.
Hot post / viral content
A viral post is read enormously; serve its media from the CDN and its metadata from the post cache so no single shard is a choke point. (The write side is already handled by pulling celebrity posts.)
12. Ranking (the real follow-up)
We built a reverse-chronological feed. A production feed adds ML ranking: the candidate posts (from push + celebrity pull) are scored by a model on features like recency, the author relationship, predicted engagement, and content type, then re-ordered. You don't need to design the model — but naming that the feed becomes rank the candidate set, not just sort by time, is the right senior-level follow-up.
13. Trade-offs, follow-ups & what to say at each level
Trade-offs to name unprompted: push vs pull vs hybrid (and the celebrity threshold); shard-by-userId vs shard-by-postId (single-shard reads vs even writes); eventual consistency (a post a few seconds late) for feed performance; feed-cache cost (cap length, lazy regen); ids-in-cache vs full posts.
Follow-ups a senior interviewer asks next: ML ranking, stories (ephemeral, TTL'd), the like/comment counters (async aggregation, like a rate limiter's counters), and explore/recommendations.
What to say at each level. Mid: media in S3+CDN, metadata in the DB, a precomputed feed. Senior: the push/pull hybrid with the celebrity threshold, the Redis-sorted-set feed cache, async fan-out, partition-by-userId, read-your-writes. Staff+: the write-amplification math, time-sortable (Snowflake) post ids that make feed ordering free, consistent-hashing resharding and the read/write service split, feed-cache cost and lazy regen, multi-region, ranking as the candidate-set re-rank — unprompted.
Why interviewers love this one
It forces the fan-out trade-off (no single right answer), separates media storage from metadata, and rewards reasoning about a read-heavy system at scale with real numbers. It's the same framework as every other "design X" — see the URL shortener for the simpler read-heavy version, or WhatsApp for the write-side fan-out of a chat group.
Amit Singh is a Senior SDE at Amazon, a Claude Certified Architect, and the instructor at AlgoEngineer. The fan-out decision trips up most candidates — we drill it live in the mock interviews in our System Design course.