- Media storage at petabyte scale.
- A read-heavy feed that has to feel instant.
- Fan-out to millions of followers.
- The ugly tail of celebrity accounts that breaks the clean version of every design.
The candidates who pass hold all four in their head at the same time and keep the trade-offs straight. That is the whole game.
If you remember five things
The whole answer in one glance. Everything below is the defense of these five calls.
- Read-optimized by design. The feed is 50:1 read-heavy, so you pay the expensive work once at write time (fan-out) and let 50 cheap reads ride on top.
- Hybrid fan-out. Push posts to followers for normal accounts, pull for the handful of celebrities, and merge the two at read time. This one call is driven entirely by that 50:1 ratio.
- Media never touches app servers. Signed-URL uploads straight to blob storage, CDN for delivery, metadata only in the database. That is what keeps 60 PB a year from eating the company.
- Hot keys get sharded and replicated. A viral post is one key read millions of times and liked millions of times, so you split the counter and replicate the content, and only when it actually goes hot.
- Eventual consistency with a read-time filter. Push the easy case eagerly, then clean up unfollows, deletes, and blocks with one cheap filter on read instead of chasing every cached feed.
1Clarifying the requirements#
Pin down scope before drawing a single box. The "right" Instagram for a 45-minute interview is a tiny slice of the real thing, and the fastest way to fail is to design all of it shallowly.
Candidate: Are we building the whole app, or should I focus on one surface?
Interviewer: Focus on the core: posting media, following users, and the home feed. Assume stories and DMs are out of scope.
Candidate: Got it. Is the feed chronological or ranked?
Interviewer: Ranked eventually, but you can start chronological and tell me where ranking plugs in.
Candidate: Last one. What is our rough scale? Hundreds of millions of users?
Interviewer: Yes. Assume 500 million daily actives. Design for that.
What those answers just told me:
- 500M DAU means the naive "read everyone I follow at feed time and merge" falls over on day one, so fan-out is the centerpiece.
- "Ranked eventually" means keep post IDs and post content separable, so a scorer can reorder IDs before hydration.
- Narrating where I plan to spend my deep-dive budget earns trust early, so I say "fan-out is the hard part" out loud now.
Functional requirements
- Upload a photo or short video with a caption.
- Follow and unfollow other users.
- See a home feed of recent posts from people you follow, newest first.
- View any single post and its like and comment counts.
- Like and comment on posts.
Non-functional requirements
- Read-heavy. Feed loads vastly outnumber posts, and I will quantify the exact ratio because it decides the architecture.
- Fast feed. Target under 200 ms server-side at p99, because above that the scroll feels sticky and engagement drops.
- Highly available. A failed write is annoying; a failed feed load is unacceptable. Lean AP over CP wherever possible.
- Eventually consistent feed. A friend's post two seconds late is nobody's bug.
- Durable media. We never lose someone's photos, full stop.
Parked on purpose (naming them is a seniority signal, not a gap): content moderation, abuse and spam detection, search, and hashtag discovery.
This opening is scored before you draw a box. I am listening for whether you pull out the read:write asymmetry and the celebrity case on your own. Open with a login flow or CRUD endpoints and I have quietly downgraded you. Name the hard part first, then go spend your time there.
2Back-of-the-envelope estimation#
Get the numbers on the board so the design stands on something instead of vibes. Round aggressively: the goal is an order of magnitude, not a balance sheet.
You are not here to compute storage to three decimals. You are here to surface one load-bearing number: reads outnumber writes about 50 to 1. That asymmetry is what justifies doing the expensive work at write time and serving nearly free on read. Find it fast, then move on.
Users and writes.
- 500M DAU. Say each posts 0.2 times per day (most people lurk, a few power users post a lot).
- That is 100M posts per day.
100,000,000 posts / 86,400 sec ≈ 1,160 posts/sec average
- Traffic is spiky around evenings and events, so plan for 3x peak: about 3,500 writes/sec.
- That is modest. Any sharded datastore handles it without breaking a sweat, which already tells me writes are not my problem.
Reads.
- Each DAU opens the feed maybe 10 times a day, one page per open.
500,000,000 users × 10 opens = 5,000,000,000 feed reads/day
5,000,000,000 / 86,400 ≈ 58,000 reads/sec average
- At 3x peak that is about 175,000 feed reads/sec.
- Read-to-write ratio is roughly 50 to 1. When reads outnumber writes 50 to 1, you pay the expensive work once on the write side and let 50 cheap reads ride on top. That one sentence is the thesis of the whole system.
Media storage.
- Average post is one image at 1.5 MB compressed, plus a 100 KB thumbnail, so call it 1.6 MB.
100M posts/day × 1.6 MB ≈ 160 TB/day
160 TB × 365 ≈ 58 PB/year
- Call it 60 PB a year of new media before replication; with 3x cross-region replication, closer to 180 PB a year on disk.
- This goes in blob storage, never a database. Putting 1.6 MB blobs in a relational row is the mistake that ends an interview, so I say explicitly: media and metadata live in different systems.
Metadata.
- A post row (id, author, caption, timestamps, counters, media pointer) is about 1 KB.
100M posts/day × 1 KB = 100 GB/day of metadata ≈ 36 TB/year
- Tiny next to media, by a factor of roughly 1,600. It fits comfortably in a sharded datastore, and that contrast is exactly why I split the two.
Feed cache (the number that drives the cost).
- Keep the latest 500 post IDs per active user in memory; each ID plus a score is about 16 bytes.
500M users × 500 IDs × 16 bytes = 4 TB
3Core APIs#
Clean REST over HTTPS, surface kept small on purpose. Every extra endpoint is a thing I have to defend and the interviewer did not ask for.
POST /v1/posts
GET /v1/posts/{postId}
GET /v1/feed?cursor={cursor}&limit=20
POST /v1/users/{userId}/follow
DELETE /v1/users/{userId}/follow
POST /v1/posts/{postId}/likes
POST /v1/posts/{postId}/comments
Upload is a two-step dance so big bytes never touch my app servers:
- Streaming 1.6 MB through an app tier at 3,500 writes/sec would waste bandwidth and turn my stateless fleet into a file proxy.
- So I hand the client a signed URL and get out of the way.
// 1) client asks for a place to put the file
POST /v1/uploads
Response: {
"uploadUrl": "https://blob.example.com/signed-put-url",
"mediaId": "md_9f2a7c"
}
// 2) client PUTs bytes straight to blob storage, then creates the post
POST /v1/posts
{
"mediaId": "md_9f2a7c",
"caption": "sunrise over the ridge",
"type": "image"
}
Two choices worth defending:
- The signed URL is scoped to one object key and expires in a few minutes, so a leaked URL is near worthless.
- The post row is created only in step two, so a half-finished upload leaves an orphan blob (cheap to garbage-collect) instead of a broken post.
The feed response is cursor-paginated, not offset:
- Offset pagination breaks when new posts arrive mid-scroll: row N shifts, the user sees duplicates or skips.
- The cursor is an opaque token encoding the score and id of the last item seen. The client treats it as a blob and sends it back.
GET /v1/feed?cursor=eyJzIjoxNjk5...&limit=20
Response: {
"items": [
{ "postId": "p_55a1", "author": "u_120", "mediaUrl": "...", "likeCount": 4120 }
],
"nextCursor": "eyJzIjoxNjk4..."
}
- Every write endpoint takes an idempotency key in a header, so a mobile retry after a flaky network never creates a duplicate post or double-like. At this scale, retries are constant, so idempotency is not optional.
4High-level design#
The box diagram is table stakes. What earns points is the single seam: a queue between the post write and the fan-out. That seam lets posting feel instant, absorbs spikes, and guarantees a fan-out backlog can never block or drop a write. Draw the boxes fast, then defend that seam.
The spine is standard, so draw it fast:
- Clients hit a load balancer, then a gateway.
- The gateway routes to three services, each owning its own data.
- Writes and reads take deliberately different paths, and media lives outside the request path entirely.
Posting.
- The client uploads bytes to blob storage directly via a signed URL, then calls Post Service to create the metadata row.
- Post Service writes to the Post Store and drops a message on the fanout queue.
- It returns 201 immediately, before any feed is touched. The post is durable even though feeds are not updated yet.
- The write path is short; the expensive fan-out happens after I have already told the user "posted."
Fan-out.
- Workers pull from the queue, look up the author's followers in the Graph Service, and push the new post id into each follower's feed list in the Feed Cache.
- This is the heavy lifting, done async off the write path, so a slow fan-out never makes a user wait on the post button.
Reading the feed.
- Feed Service reads a page of post ids straight from the Feed Cache, hydrates them with cached metadata, and returns.
- No joins, no scanning a follows table at read time.
- A feed read is one sorted-set range query plus a batched metadata lookup. That is how I keep 175k reads/sec under 200 ms.
Serving media.
- Images and videos are served by a CDN, not my servers. Blob storage is the origin.
- The vast majority of media bytes never hit my infrastructure twice, which keeps 60 PB a year from turning into a bandwidth bill that eats the company.
Why split the services this way.
- Post Service owns post metadata, Graph Service owns follow relationships, Feed Service owns assembling and serving feeds.
- Their load profiles differ: Feed is read-bound at 175k/sec, Post is write-bound at 3.5k/sec, Graph gets hammered in bursts during fan-out.
- Coupling them into one service would mean scaling all three for the worst case of any one.
Sortable unique IDs (Snowflake).
- 64 bits: a timestamp in the high bits, a machine id, and a per-ms sequence in the low bits.
- Roughly time-ordered, so I get sortable ids for cursoring and shard distribution for free.
- Generated without a central counter, so there is no id-allocation service to become a bottleneck or a single point of failure.
5Data model#
Use a few stores, each picked for its access pattern. Picking the right store per workload matters more here than finding one database to rule them all.
- Sharded relational-style store for metadata (sharded by a hash of the id).
- Wide-column or key-value store for the follow graph and feed lists.
- Blob storage for media, referenced by key only.
posts
| Field | Type | Notes |
|---|---|---|
| post_id | bigint | Snowflake id, sortable by time, shard key |
| author_id | bigint | indexed |
| media_key | string | pointer into blob storage |
| media_type | enum | image or video |
| caption | text | up to 2200 chars |
| created_at | timestamp | drives chronological ordering |
| like_count | bigint | denormalized counter |
| comment_count | bigint | denormalized counter |
follows
| Field | Type | Notes |
|---|---|---|
| follower_id | bigint | shard key, "who I follow" lookups |
| followee_id | bigint | indexed for "who follows me" |
| created_at | timestamp | for ordering and anti-spam |
- Store the edge twice, keyed by follower and by followee, because I need both directions: "who does user X follow" at read time, and "who follows author Y" during fan-out.
- Both directions are append-heavy, so the extra write is trivial next to the read savings.
feed (in cache, per user)
| Field | Type | Notes |
|---|---|---|
| user_id | key | the feed owner |
| post_id | member | element in a sorted set |
| score | number | created_at or ranking score, sort key |
- A Redis sorted set capped at the latest 500 entries; trim past the cap with a single
ZREMRANGEBYRANK, so per-user memory stays at the 16 bytes times 500 I budgeted. - IDs only, not full posts, so a ranking layer can reorder IDs before hydration.
- A cold user (returning after a month) with no cached feed falls back to on-demand compute from the people they follow, backfilling the sorted set.
likes and comments get their own tables keyed by post_id, with counts denormalized onto the post row:
- A feed page shows 20 posts; counting likes live for each would be 20 aggregate queries per read times 175k reads/sec, which is absurd.
- The denormalized counter turns that into a single field read.
6Deep dives#
6.1 Fan-out: the central decision#
The whole interview turns on which feed strategy you pick. State both, commit, then defend the commitment. A senior answer is not "it depends," it is "here is my choice and here is exactly when I would revisit it."
- Fan-out on write (push). At post time, copy your post id into every follower's feed list. Reads become trivial: just read your list. Writes get expensive: one post by someone with 10,000 followers means 10,000 cache writes.
- Fan-out on read (pull). Write the post once. At feed time, gather who you follow, pull their recent posts, and merge. Writes are cheap; reads are slow, because a merge across hundreds of authors is real work on the hot path running at 175k/sec.
Fan-out on write (push)
- Read cost
- Trivial: read one precomputed list.
- Write cost
- Heavy: one post touches every follower's list.
- Best when
- Reads dominate and most accounts are normal-sized.
- Breaks when
- A celebrity posts: one write becomes tens of millions.
Fan-out on read (pull)
- Read cost
- Heavy: fetch and merge from everyone you follow.
- Write cost
- Trivial: store nothing per follower.
- Best when
- Writes dominate, or the author has a huge following.
- Breaks when
- A read-heavy feed makes every load expensive.
For a 50:1 read ratio, push wins almost everywhere: precompute the expensive thing once at write, serve it 50 times for free. So push is my default.
Reveal the answer
So I go hybrid.
- Default to push, because reads dominate and I want them cheap.
- Set a threshold, say 50,000 followers. Above it, the account is marked "wide" and I do not fan out their posts.
- At feed time, Feed Service merges the reader's precomputed push feed with a small pull from the handful of wide accounts they follow.
- Most people follow only a few dozen wide accounts, so the pull side stays tiny and fast.
The threshold is a knob, not a law.
- Raise it and I shift load onto fan-out workers; lower it and I shift load onto feed reads.
- The merge touches maybe a few dozen wide accounts at most, not hundreds, so the read stays comfortably inside 200 ms.
- Tune it from production fan-out latency and queue-depth metrics, and ideally make it dynamic per posting rate, not just follower count.
Fan-out is at-least-once, and that is fine here by construction.
- A worker retry can push the same post id twice.
- No dedup table needed: the feed is a sorted set scored by post_id, so re-adding an existing id is a no-op.
- Idempotency falls out of the data structure. Say this out loud; the interviewer is watching for whether you noticed retries can duplicate.
Committing to pure push, feeling clever about cheap reads, then freezing when I say "a celebrity just posted." Reach for hybrid on your own, and say why in numbers. Candidates who wait to be told about the celebrity case are the ones who have not actually run this system.
6.2 The hot-key problem#
When a celebrity posts, two things catch fire at once, and they fail in opposite directions.
- Read hot key. Millions open that single post, so one post_id becomes a read key that can melt one cache shard, because hashing by post_id sends every read to the same node.
- Write hot key. Likes pour in and the like_count counter becomes a write key on the same row.
Fix the read hot key by spreading it:
- Replicate hot post metadata across multiple cache nodes and let the client library pick a replica at random, spreading reads across, say, 10 copies so each sees a tenth of the storm.
- Push popular media aggressively to the CDN edge, so the bytes never come back to origin.
- A post with 20 million views costs me almost nothing after the first few seconds: the 21st viewer and everyone after is served from an edge node near them, not from blob storage.
Fix the write hot key by sharding the counter:
- Never run
UPDATE posts SET like_count = like_count + 1under viral load. That row becomes a lock convoy: every like serializes on the same row lock and throughput collapses. - Split the like_count for a hot post into, say, 100 sub-counters across different keys. A like increments a random sub-counter, so load spreads across 100 keys instead of piling onto one.
- The displayed total is the sum, computed periodically and cached. Readers see a slightly stale count, which is fine: nobody notices 2,041,180 instead of 2,041,195.
Promote to the hot path only when a post actually goes hot.
- The vast majority of posts never go viral, so running 100 sub-counters for every post wastes memory for no reason.
- Cold posts use one counter; hot posts get split on the fly once they cross a like-rate threshold.
6.3 Keeping the feed consistent-enough#
Follows and unfollows race against fan-out.
- Unfollow someone and their already-pushed posts are still sitting in my feed cache.
- Scrubbing every unfollow out of every cached feed is expensive, and at my unfollow rate it is not worth the write traffic.
- So I filter at read time: Feed Service checks each candidate post against the reader's current follow set before returning the page.
- The follow set is small (a few hundred accounts) and already cached, so the check is a cheap set-membership test. It quietly fixes stale entries from unfollows, blocks, and deletes in one pass.
Deletes work the same way.
- Flip a tombstone flag on the post row and let the read-time filter drop it.
- I do not chase it across millions of feed lists: that is the same write-amplification disaster I just spent a deep dive avoiding.
- Stale cache entries age out naturally when feeds get trimmed past 500.
The key philosophy, stated exactly as I would in the room:
- Push the easy case eagerly, and let a cheap read-time filter clean up the messy edges.
- It is far simpler than keeping every cached feed perfectly in sync, and it fits the eventual-consistency model I agreed to up front.
7Bottlenecks and trade-offs#
The fanout queue is the first thing to break.
- A spike, or a batch of mid-size accounts posting at once, backs up the queue and feeds go stale.
- Make fan-out fully async and idempotent (replaying a job re-writes the same ids, so a retry is harmless).
- Autoscale workers on queue lag (age of the oldest unprocessed job), not raw depth, because depth lies during a burst of tiny events.
- Accept that staleness under load is a feature, not an outage: a feed a few minutes behind beats the whole write path stalling. Alarm on queue depth and p99 fan-out lag so I see pressure before users do.
The follow graph is a scaling trap.
- Fan-out reads follower lists constantly, so the Graph Store gets hammered during posting storms.
- Cache follower lists for active authors and shard the graph by follower_id.
- The hybrid threshold protects the graph directly: I never read the 40-million-entry follower list of a celebrity for fan-out at all. That single rule removes the worst graph read in the system.
Precomputed feeds waste work on inactive users.
- Fan out to 40,000 followers but only 2,000 open the app today and you did 38,000 writes nobody read.
- So I only fan out to recently active users (say active in the last 7 to 30 days) and lazily build the feed for someone returning after weeks, using the same on-demand fallback as cold feeds.
- This cuts fan-out volume by more than half in practice, because the active tail is always smaller than the total follower count.
Ranking is where chronological bites back.
- A ranked feed needs signals (recency, affinity, predicted engagement) that change per request and cannot be fully precomputed.
- Keep fan-out producing a candidate set of a few hundred posts, then run a lightweight ranking pass at read time over just those candidates.
- Rank the small set, not the whole world: scoring 300 candidates per read is affordable, scoring every post a user could see is not.
- That keeps p99 in budget and gives the ranking team a clean place to plug in a model without touching the feed plumbing.
Strong consistency is the thing I deliberately give up.
- Your like count is approximate, your feed may be seconds stale, an unfollowed post may flash before the filter catches it.
- I trade all of that for availability and speed, the correct trade for a social feed where nothing a user sees is load-bearing for money or safety.
- The one place I do not compromise is media durability: blob storage with cross-region replication, because losing photos is the one unforgivable failure.
Single points of failure.
- The gateway and load balancer are replicated across availability zones.
- The caches are replicated, not just sharded, so losing a node degrades rather than drops traffic.
- The honest risk: a thundering herd if the whole feed cache cold-starts after a region-wide restart, when every read misses and falls through to the on-demand path, overwhelming the Post Store.
- Mitigate with staggered warming, request coalescing on the fallback path, and an on-demand builder that acts as a release valve rather than a cliff.
Always let the read:write ratio pick the architecture, not habit.
8Summary#
The decisions I would stand behind, with the one-line reason for each:
- Read-optimized by design. A 50:1 read ratio means I do work at write time (fan-out) so feed reads are a simple cache lookup. This is the decision everything else hangs off.
- Hybrid fan-out. Push for normal accounts, pull-and-merge for the handful of celebrity accounts above a 50,000-follower threshold. Kills the write-amplification disaster without slowing normal reads.
- Media never touches app servers. Signed-URL uploads to blob storage, CDN for delivery, metadata-only in the database. This keeps 60 PB a year affordable.
- Hot keys get sharded and replicated. Sharded like counters for viral writes, replicated hot-post caches for viral reads, promoted only when a post actually goes hot.
- Eventual consistency with a read-time filter. Push the easy case, clean up unfollows, deletes, and blocks cheaply at read time instead of chasing them across every feed.
- Durability is the one hard guarantee. Everything else can be stale or approximate; photos cannot be lost.
- Out of scope by choice: moderation, abuse detection, search, hashtag discovery, stories, DMs. Naming them keeps me honest about what 45 minutes covered.
"A celebrity with 40 million followers posts. Walk me through what happens."
"A worker retries and pushes the same post id twice. Duplicate in my feed?"
"A viral post gets a million likes a minute. How do you not melt one row?"
"I unfollow someone. Their posts are already in my cached feed. Now what?"
"The interviewer flips the feed to ranked. Where does ranking plug in?"
What you can now explain in the room
- Why a 50:1 read ratio pushes the expensive work to write time and keeps reads a cache lookup.
- Why pure push breaks on celebrities, and how the 50,000-follower hybrid threshold fixes it.
- How to derive the load numbers: 175,000 reads/second at peak, 60 PB a year of media, and the 4 TB feed cache.
- Why media uploads use signed URLs and a CDN so big bytes never touch app servers.
- How a viral post creates read and write hot keys, and how replication and sharded counters defend each.
- Why unfollows, deletes, and blocks are cleaned up by a cheap read-time filter instead of chasing every cached feed.
For how specific companies push on these trade-offs, see our related company-specific system design guides.
Get a prep plan built around your Design Instagram interview
Tell us the role and your interview date. We will send back a dated, day by day plan. Free.