SYSTEM DESIGN · DESIGN PROBLEMS · FEEDS AND SOCIAL · MEDIUM · 10 OF 41

Design Instagram: Photo Uploads, the Home Feed and the Celebrity Problem

A step-by-step design for a photo-sharing app with a home feed: requirements, scale math for half a billion daily users, the upload API with pre-signed URLs, async resizing into several sizes, a CDN for images, hybrid push and pull fan-out for the feed, the celebrity problem, feed caching and cursor pagination, and the trade-offs to name.

Reported atMeta

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
1 / 18

Press play to hear the narration, or step through at your own pace.

Notes

Listen

Start by fixing the scope out loud. Users upload a photo with a caption, follow other users, and scroll a home feed of posts from people they follow. Likes and comments exist, but we treat them as counters. Stories, reels, direct messages, ads and search are out of scope, and you say so, so the interviewer can push back now rather than at minute thirty.

Now the qualities. The feed must feel instant, say under five hundred milliseconds at p ninety-nine. Availability beats consistency: showing a feed that's a few seconds stale is far better than an error. So eventual consistency is fine for the feed. The one thing we must never do is lose an uploaded photo once we've told the user it posted.

Functional requirements
  • in scope:
  • • upload a photo with a caption
  • • follow and unfollow users
  • • home feed of followed posts
  • • like and comment (counts)
  • • view a profile grid
  • out of scope today:
  • • stories, reels, DMs, ads
  • • search and explore
Non-functional requirements
  • feed loads in under 500 ms
  • p99, mostly from cache
  • highly available: a stale feed
  • beats an error page
  • eventual consistency is fine:
  • a post can take seconds to
  • reach followers
  • uploads must never be lost

Ask scale

Assume five hundred million daily users who open the feed ten times a day. That's five billion feed reads a day. Divide by roughly a hundred thousand seconds in a day and you get fifty thousand requests a second, about a hundred thousand at peak. Uploads are a hundred million a day, around a thousand a second. Reads outnumber writes about fifty to one, so we optimise the read path.

Storage: each photo is about two megabytes, plus another megabyte of resized copies. A hundred million a day is three hundred terabytes daily, over a hundred petabytes a year. Metadata is tiny by comparison, about a kilobyte a post. Bandwidth is the scary one: ten petabytes a day of images going out. No origin can serve that. This is why images live in object storage and leave through a CDN.

Traffic
DAU                 500M
feed opens/user/day 10
feed reads/day      5B
  5B / 100,000 s   = 50K rps
  peak x2          = ~100K rps

uploads/day         100M
  100M / 100,000 s = 1K wps
  peak x3          = ~3K wps

read:write          ~50:1
Storage and bandwidth
photo original      2 MB
resized variants    +1 MB
100M x 3 MB       = 300 TB/day
  x 365           = ~110 PB/yr

metadata 1 KB/post = 100 GB/day

feed view: 20 imgs x 100 KB
  = 2 MB per feed open
  5B x 2 MB       = 10 PB/day
  = ~115 GB/s out  -> CDN

Define API

Four or five endpoints cover it. The client asks for an upload slot and gets back a pre-signed URL, then puts the photo bytes directly into object storage, so our API servers never carry megabytes of image data. Then it creates the post with the upload ID and a caption. The feed is a GET with a cursor and a limit. Follow and like are simple POSTs, and every write takes an idempotency key so retries don't double-post.

Core API
POST /v1/uploads
  -> { uploadId, presignedUrl }
PUT  presignedUrl   (bytes to storage)
POST /v1/posts
  { uploadId, caption }
  -> { postId }
GET  /v1/feed?cursor=..&limit=20
  -> { posts[], nextCursor }
POST /v1/users/{id}/follow
POST /v1/posts/{id}/like

Data model

The data model. Users and posts are small records. Posts are sharded by post ID, with a time-sortable ID so newest-first is cheap, and an index by author for the profile grid. The follow graph is stored twice, once keyed by follower and once by followee, because we need both directions fast. A wide-column store like Cassandra suits posts and follows: huge volume, simple key lookups. The precomputed feed is just a list of post IDs per user in Redis.

Main records
users    (userId PK, name, bio)
posts    (postId PK, authorId,
          mediaKeys, caption, ts)
  shard by postId; index authorId
follows  (followerId, followeeId)
  two tables: by follower and
  by followee
feed     userId -> [postId..]
  in Redis, capped at ~800
likes    counter per postId

High-level design

Draw the upload path first. The app gets a pre-signed URL through the gateway and uploads the photo straight to object storage. Storage fires an object-created event, and media workers resize it into a thumbnail, a feed size and a full size, and strip location metadata. Meanwhile the app creates the post, and the post service writes the metadata row. The post is visible once the resized images exist.

Now the feed path. When a post is created, an event goes onto a queue. Fan-out workers look up the author's followers and push the new post ID onto each follower's feed list in the cache. When you open the app, the feed service reads your list, hydrates the top twenty IDs into full posts from a post cache, and returns image URLs. The app then pulls the images from the CDN, never from our servers.

Deep dive: media

First deep dive: the media pipeline. Resizing is slow and bursty, so it runs on a worker pool fed by a queue, scaled on queue depth. Each photo becomes a few fixed sizes in a modern format, and location data is stripped for privacy. Variants get immutable URLs that include the post ID and size, so the CDN can cache them for a year. A failed resize is just retried, because writing the same variant twice is harmless.

Why bother with pre-signed URLs? Because two gigabytes a second of image bodies, a thousand two-megabyte uploads, flowing through API servers would make them slow and expensive. Direct uploads keep the API small and stateless. On flaky mobile networks, use multipart uploads so a dropped connection resumes instead of restarting. And if someone uploads but never creates the post, a lifecycle rule cleans up the orphan after a day.

Media pipeline
  • 1 upload original to storage
  • 2 event -> resize queue
  • 3 workers make 150, 640,
  • 1080 px (WebP or AVIF)
  • 4 strip EXIF location data
  • 5 write variants, mark ready
  • CDN key: /p/{postId}/{size}
  • immutable, cache for a year
  • workers retry; idempotent
Why pre-signed uploads
  • API servers never hold 2 MB
  • bodies: cheap, stateless
  • resumable or multipart for
  • slow mobile networks
  • storage checks size + type
  • URL expires in ~15 minutes
  • orphan uploads with no post:
  • lifecycle rule deletes them

Deep dive: fan-out

Second deep dive, the heart of the problem: fan-out. With push, we do the work at write time: the median user has a few hundred followers, so a post means a few hundred small list writes, and every feed read becomes a single cache hit. With pull, we do nothing at write time and merge everyone you follow when you open the app, which is cheap to write but expensive on every read. Since reads dominate fifty to one, push is the default.

Push vs pull
  • push (fan-out on write):
  • write postId into each
  • follower feed list
  • + reads are one cache hit
  • - big accounts = huge writes
  • pull (fan-out on read):
  • merge followees at open time
  • + writes are cheap
  • - every open does N lookups

Deep dive: celebrities

Push breaks for celebrities. One post from an account with a hundred million followers means a hundred million writes, minutes of lag and a jammed queue. The standard fix is a hybrid. Accounts under a threshold, say ten thousand followers, are pushed. Above it, we skip fan-out. At read time, the feed service takes your precomputed list and merges in recent posts from the handful of celebrities you follow, which are hot and sit in cache anyway.

The celebrity problem
  • one post x 100M followers
  • = 100M list writes, minutes
  • of lag, a hot queue
  • hybrid rule:
  • • under ~10K followers: push
  • • over: skip fan-out, store
  • only in author timeline
  • • at read: merge my cached
  • feed + recent celeb posts

Deep dive: feed reads

Serving the feed. Each list holds only the newest several hundred IDs, which keeps memory bounded. Paginate with a cursor, the last post ID you saw, not with page numbers, because new posts arriving at the top would shift every page. Hydration is a batched multi-get from a post cache. We don't fan out to users who haven't opened the app in a month; when they return, we rebuild their feed with a pull. Ranking, if asked, scores the top few hundred candidates with a model.

Serving the feed
  • list keeps newest ~800 IDs
  • cursor = last postId seen
  • (stable when new posts land)
  • hydrate: multi-get post cache
  • then like counts, author info
  • skip fan-out to users idle
  • 30+ days; rebuild on login
  • ranking: score top ~500 IDs

Trade-offs

Trade-offs and failures. If a feed cache node dies, those users get a slower pull-based rebuild once, not an outage. If fan-out workers fall behind, feeds lag but nothing is lost, because the queue is durable. A viral post is a hot key, so replicate it across cache nodes. What to monitor: fan-out lag from post to follower, feed cache hit rate, p ninety-nine feed latency and resize queue depth.

Name what you traded. Precomputed feeds duplicate post IDs millions of times, spending memory to buy fast reads. Eventual consistency spends a few seconds of freshness to buy availability. At ten times the scale, lower the celebrity threshold so fewer posts fan out, shard the feed cache by user ID, run regions that each serve their own users, and move old originals to cheaper cold storage while keeping the small sizes hot.

Failure modes
  • feed cache node lost:
  • rebuild by pull, slower once
  • fan-out backlog:
  • feeds lag, nothing is lost
  • hot post (viral): replicate
  • its cache key, CDN absorbs
  • the images
  • region down: serve feeds from
  • replica region, read-only
  • monitor: fan-out lag, hit rate
At 10x and the trade-offs
  • we traded: storage (feeds are
  • copies of IDs) for fast reads
  • we traded: freshness (seconds)
  • for availability
  • 10x: lower the celeb threshold
  • shard feed cache by userId
  • multi-region, local writes
  • tier old photos to cold
  • storage, keep thumbs hot

Say it like this

Here's the summary you'd give. Photos go directly to object storage with a pre-signed URL, get resized by background workers and are served through a CDN. Metadata sits in a sharded store. The feed is fan-out on write into per-user lists of post IDs, with a hybrid pull for celebrity accounts merged at read time. Cursor pagination, batched hydration, and eventual consistency because reads matter more than instant freshness.

The 60-second answer
  • upload: pre-signed URL ->
  • object storage -> resize
  • workers -> CDN
  • metadata: sharded wide-column
  • feed: fan-out on write into
  • Redis lists of post IDs
  • celebs: pull + merge at read
  • cursor pagination, hydrate
  • from post cache
  • eventual consistency, fine

Interview questions

Would you use fan-out on write or fan-out on read for the home feed?

Fan-out on write for most users, because reads outnumber writes by roughly fifty to one and a precomputed list makes each feed open a single cache read. For accounts above a follower threshold I switch to fan-out on read and merge their recent posts at request time.

How do you handle a user with 100 million followers?

Their posts skip fan-out and live only in their own timeline, which is heavily cached. When a follower opens the feed, the service merges the follower’s precomputed list with recent posts from the few celebrities they follow.

Why upload photos with a pre-signed URL instead of through the API?

It keeps multi-megabyte bodies off the API servers, so they stay small, stateless and cheap. Object storage handles the bytes, supports multipart resumable uploads, and the URL expires quickly to limit abuse.

How do you paginate the feed?

With a cursor that encodes the last post ID returned, since post IDs are time-sortable. Offsets break when new posts are inserted at the top, while a cursor always continues from the same point.

Where do you store photos versus metadata?

Photos and their resized variants go into object storage and are served by a CDN with immutable URLs. Post metadata, follows and counters go into a sharded database such as Cassandra, with hot items cached in Redis.

What happens if the feed cache loses a node?

The affected users’ feeds are rebuilt on their next open by pulling recent posts from the people they follow, which is slower once but correct. The cache is a derived copy, so losing it never loses data.

How would you add ranking instead of a chronological feed?

Keep the same candidate generation, the fan-out list plus celebrity posts, then score the top few hundred candidates with a model using signals like affinity, recency and engagement. Cache the ranked result for a short time so pagination stays stable.

How do you make sure an upload is never lost?

The post is only confirmed after the original is durably written to replicated object storage and the metadata row is committed. Resizing is asynchronous and retried, and post creation uses an idempotency key so a retry does not create a duplicate.