Back to Blog
System DesignInterview TipsDistributed SystemsFAANG

Design Instagram — System Design Interview Walkthrough

Amit Singh

Amit Singh

Author

June 25, 2026
19 min read

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. So 250M/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, so 58,000 feed-loads/sec × ~2 MB ≈ ~115 GB/s of 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:

High-level architecture
ClientPost serviceFan-outqueueFan-outworkerFeed cacheRedis ZSETFeed servicePosts DBpostenqueueZADDmetadataGET feedZREVRANGEceleb pull
Write path: the post service writes metadata and enqueues a fan-out job; a worker pushes the post id into each follower's Redis feed. Read path: the feed service reads the precomputed feed and merges in recent celebrity posts.

5. The crux: generating the feed (fan-out)

This is where the interview is won. Two strategies:

StrategyHowProsCons
Fan-out on write (push)When you post, write the post id into every follower's precomputed feed listFeed reads are O(1) and instantA 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 followCheap writes; no wasted work for inactive usersExpensive, 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

interactive
PUSH
fan-out on write
805
feed writes / post

With 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:

Fan-out: write and read paths
write path (fan-out)read pathcreate postwrite metadataenqueue fan-outdequeueZADD to each followerGET /feedZREVRANGEpull celebrity postsmerged feed + CDN urlsClientPost svcQueueWorkerFeed cacheFeed svcPosts DB
Write path: the post service writes metadata and enqueues a fan-out job; a worker pushes the post id into each follower's feed. Read path: the feed service reads the precomputed feed and merges in recent celebrity posts.
  1. 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.
  2. 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).
  3. 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 keyWhat it's good atWhat breaks
By userId"All of a user's posts" and "a user's follow list" are single-shard readsHot 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 postIdWrites 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.

Design Instagram in a system design interview

The moves for designing a read-heavy social feed at scale.

  1. 1

    Clarify requirements and scale

    Pin upload, follow, home feed, likes/comments; scope out stories/DMs/ads. Establish that the system is read-heavy and the feed is the hard part.

  2. 2

    Estimate scale

    Daily posts → write QPS, feed loads → read QPS (reads dominate), and media volume → object storage + CDN, not the database.

  3. 3

    Separate media from metadata

    Photos go to S3 + CDN via presigned uploads; the database stores metadata and the media URL.

  4. 4

    Decide the fan-out strategy

    Compare push (fan-out on write) and pull (fan-out on read); land on the hybrid that pushes for normal users and pulls for celebrities.

  5. 5

    Store the feed concretely

    A per-user Redis sorted set of post ids by timestamp, written by an async fan-out worker and capped at the latest few hundred.

  6. 6

    Name the hard parts

    Celebrity write amplification with real numbers, feed-cache cost, eventual consistency, ranking, and failure modes.

Frequently asked questions

What is the hardest part of designing Instagram?
Generating each user’s home feed at scale. Photo upload, following, and likes are warm-ups; the interview is decided by the fan-out decision — whether you precompute feeds when someone posts (push), build them at read time (pull), or use a hybrid that pushes for normal users and pulls for celebrities. Naming that hybrid and the threshold that triggers it is the signal interviewers look for.
What is fan-out on write vs fan-out on read?
Fan-out on write (push) means that when you post, the system immediately writes your post id into every follower’s precomputed feed, so their reads are instant — but a celebrity with millions of followers triggers millions of writes per post. Fan-out on read (pull) builds the feed at read time by querying recent posts from everyone you follow — cheap writes, but slow, expensive reads. The hybrid uses push for normal accounts and pull for celebrities.
How is the feed actually stored?
As a per-user Redis sorted set: the member is a post id and the score is the timestamp, so writing a post is ZADD and reading the latest page is ZREVRANGE. The cache stores only post ids (not full posts), capped at the most recent few hundred, and the post bodies are hydrated from a post cache on read. Inactive users’ feeds are regenerated lazily rather than kept warm.
Where are the photos stored?
In object storage (S3) plus a CDN, never in the database. The client uploads the image directly to S3 via a presigned URL (bypassing the app servers’ bandwidth), the server stores only the metadata and the media URL, resized variants (thumbnail/feed/full) are generated on ingest, and the feed returns CDN URLs so images are served from an edge near the user.
What is the celebrity problem and how do you solve it?
A celebrity with tens of millions of followers would trigger tens of millions of feed writes per post under pure push — and posting several times a day makes that hundreds of millions of writes, which is unsustainable. The fix is the hybrid: above a follower threshold (say ~10k–100k), stop pushing their posts and instead pull them at read time, merging a celebrity’s recent posts into the feed when a follower loads it.
How do you partition the metadata and generate post ids?
Partition the posts and follows by userId, not by postId — you almost always read a user’s posts and follow list together, so co-locating them on one shard keeps those reads single-shard, and consistent hashing lets you add or lose a shard without a full reshard. Sharding by postId spreads writes evenly but turns "a user’s posts" into a scatter-gather and creates a chicken-and-egg: if the id picks the shard, you can’t mint it with a per-shard counter. Generate post ids with a Snowflake-style, time-sortable scheme (timestamp + worker id + sequence) minted locally with no coordination — and because the id embeds time, the feed’s newest-first ordering falls out of the primary key for free.

Ready to Ace Your Interviews?

Live, cohort-based interview prep taught by a working FAANG engineer — weekly mock interviews, lifetime access, and a 7-day money-back guarantee.