Design Instagram
Design a photo and short-video sharing app. Users upload photos or clips with a caption, follow other people, scroll a home feed of posts from accounts they follow, like and comment on posts, and post stories: photos or clips that appear in a tray at the top of the app and vanish after 24 hours.
The service has around 500 million daily users, most on phones with uneven mobile connections. Media dominates everything: storage grows by tens of terabytes a day, and images must load quickly anywhere in the world on a slow network. A post from a popular creator can collect a million likes in an hour.
In 45 minutes, design the upload and media pipeline, the home feed, likes and stories, and explain how media reaches phones quickly.
Sketch on the HLD board, write your answer in the Design doc, and size it with the Estimator. Start the mock interview from the timer for a 45-minute run.
Design Path · Social & Messaging · problem 10 of 70
Ask before you design. Try answering each yourself, then compare.
- What media types and sizes?
Typical answer
Photos up to about 10 MB and videos up to 60 seconds. We store the original and several resized versions.
- What scale?
Typical answer
500 million daily users, about 50 million new posts and 100 million new stories a day.
- Is the home feed ranked?
Typical answer
Yes, but a simple ranking by recency and affinity is enough here; the model itself is out of scope.
- Must like counts be exact and instantly consistent?
Typical answer
No. Counts can lag by a few seconds, but a user must always see their own like immediately.
- Can stories be seen by everyone?
Typical answer
Only by followers (or close friends, as an option). They expire after 24 hours, and the owner can see who viewed them.
- Do we need private accounts?
Typical answer
Yes: posts from a private account are visible only to approved followers. Treat it as a visibility check on read.
Requirements
Functional
- Upload a photo or short video with a caption; view a post with its likes and comments.
- Follow and unfollow users; scroll a home feed of posts from followed accounts.
- Like, unlike and comment on posts.
- Post stories that followers see in a tray for 24 hours, with a viewer list for the author.
Non-functional
- Feed page (metadata) p99 under 250 ms; first image visible under 1 s on a typical 4G connection.
- Uploaded media is never lost (11 nines durability); 99.95% availability for browsing.
- A new post reaches followers' feeds within 10 s; a new story appears in trays within 10 s.
- Scale to 500 M DAU and ~1 PB/day of media egress, mostly from the CDN.
Out of scope
- Direct messages, reels recommendations and ads.
- Content moderation models (mention as an async step).
- Filters and editing on the device.
Back-of-the-envelope estimates
- Uploads: 50 M posts/day ≈ 600/s (about 1,800/s at peak), plus 100 M stories/day ≈ 1,200/s.
- Media storage: original plus renditions ≈ 1.5 MB per photo post ≈ 75 TB/day for posts, about 27 PB/year. Stories add a similar amount but are deleted from the hot tier after 24 hours (kept briefly for the author's archive if enabled). Erasure coding rather than 3 full copies keeps the overhead near 1.4×.
- Reads: 500 M users viewing ~100 items/day ≈ 50 B media fetches/day ≈ 580,000/s. Most fetches are thumbnails and compressed feed-size images averaging around 20 KB, so egress is roughly 1 PB/day ≈ 100 Gbps on average and 300 Gbps at peak, with short videos on top of that, nearly all served by the CDN.
- Metadata: 50 M posts × ~1 KB ≈ 50 GB/day; tiny compared with media.
- Likes: billions per day; a single viral post can see 1,000+ likes/s.
Media bandwidth and storage drive the design (CDN, renditions, direct-to-blob upload); metadata is a normal sharded database problem.
API
POST /v1/media/uploads {type, sizeBytes} -> {mediaId, uploadUrl (pre-signed, 15 min)}
PUT {uploadUrl} <bytes> -> 200 (straight to blob storage)
POST /v1/posts {mediaIds[], caption} -> 201 {postId}
GET /v1/feed?cursor= -> {posts:[{id, author, mediaUrls, likeCount, likedByMe}], nextCursor}
PUT /v1/posts/{id}/like -> 204 (idempotent; DELETE to unlike)
POST /v1/stories {mediaId, audience} -> 201 {storyId, expiresAt}
GET /v1/stories/tray -> {rings:[{author, storyIds[], unseen}]}Uploads use pre-signed URLs so large media bytes go straight from the phone to blob storage; the API servers only handle small JSON requests, and a like is a PUT so retries over flaky networks cannot double-count.
Data model
posts(post_id PK, author_id, caption, media_keys, created_at, visibility) and user_posts(author_id, created_at DESC, post_id) in a sharded relational or wide-column store, sharded by author_id so a profile page is one shard.
media(media_id, owner_id, status, renditions{size: blob_key}) tracks upload and processing state. The bytes live in object storage under content-addressed keys and never change, so CDN caching is safe forever.
likes(post_id, user_id, created_at) partitioned by post_id (for 'who liked this' and idempotency), plus like_counts(post_id, count) maintained by aggregated increments in a counter store.
stories(author_id, story_id, media_key, expires_at) in a key-value store with a native 24 h TTL, and story_views(story_id, viewer_id) with the same TTL. Follows are stored in both directions in a graph store. Feeds are Redis sorted sets of post ids.
High-level design
Upload. The app asks the Post Service for an upload slot and receives a pre-signed URL, then PUTs the bytes directly to the Media Blob Store. When the object lands, the store emits an event; Transcode Workers read the original, strip metadata, produce several sizes and formats (thumbnail, feed size, full screen, short video renditions) and write them back under immutable keys. The app then creates the post. The Post Service writes the post row to the Post Metadata DB and publishes a post created event.
Feed. Fan-out Workers consume the event, fetch the author's followers from the Follow Graph, and add the post id to each active follower's list in the Feed Cache. Very popular authors are skipped and merged in at read time. When the app opens, the Feed Service reads the cached ids, merges in recent posts from large accounts, ranks them, hydrates metadata and like counts, and returns CDN URLs. The phone fetches images from the CDN, which pulls from blob storage on a miss.
Stories. The Story Service writes stories to a TTL store. The tray is assembled on read: take the viewer's following list, look up which of those authors have live stories (a small 'has active story' set), and order rings with unseen ones first.
Open the HLD board and press Reference to see the architecture diagram.
Deep dives
Media pipeline and delivery
Never proxy media through API servers: pre-signed uploads keep app servers small and let the blob store absorb bandwidth. Support resumable chunked uploads so a dropped mobile connection does not restart a 50 MB video. After upload, transcoding is asynchronous and idempotent per media id; the post can be created immediately and shows a placeholder until the feed-size rendition is ready, usually within seconds. Renditions use modern compressed formats and the client asks for the size that matches its screen. Keys are content-addressed and immutable, so the CDN can cache them with very long lifetimes and deletes are handled by removing references and purging, not by editing objects.
Likes on viral posts
Write each like as an idempotent row keyed by (post id, user id), which both deduplicates retries and answers 'did I like this'. The count is separate: like events go to a stream, and a counter worker aggregates them per post every second or so before adding the delta to the counter store. For extremely hot posts, the counter is split into several sub-counters on different shards and summed on read. The app shows the user's own like optimistically, so read-your-own-writes holds for the person who tapped even though the global count lags a little.
Stories that expire
Expiry is a storage feature, not a cron job: story rows and view records carry a 24-hour TTL so the store deletes them on its own, and media keys for stories sit in a bucket with a short lifecycle rule. The tray is computed on read because stories are short-lived and very read-heavy: for each viewer, intersect the people they follow with a cached set of authors who currently have live stories, then order by unseen first and affinity second. Seen state is a small per-viewer map of author to last-seen timestamp. Because a user follows at most a few thousand accounts, this intersection is cheap, and there is no fan-out to undo when stories expire.
Failure modes
- If transcoding is backed up, posts still publish with placeholders and the original is served at a reduced quality until renditions catch up.
- If a CDN region degrades, DNS steers users to another CDN provider or region; origin capacity must be sized to absorb a cold cache burst.
- If the Feed Cache loses a shard, feeds for those users are rebuilt on read from followees' recent posts while the cache refills.
- If an upload completes but the post is never created, a cleanup job deletes orphaned media after a day.
Core algorithm in code
STORY_TTL = 24 * 3600
def build_tray(following, stories, seen, now, affinity):
"""Return the story tray: one ring per followed author who has a live story.
Authors with unseen stories come first, then by affinity, then by newest story.
stories: {author: [created_ts, ...]}, seen: {author: last_seen_ts}."""
tray = []
for author in following:
live = [t for t in stories.get(author, []) if now - t < STORY_TTL]
if not live:
continue # expired or never posted: no ring
newest = max(live)
unseen = newest > seen.get(author, 0)
tray.append((not unseen, -affinity.get(author, 0.0), -newest, author, len(live)))
tray.sort()
return [(a, "unseen" if not s else "seen", n) for s, _, _, a, n in tray]
NOW = 1_700_000_000
H = 3600
following = ["ana", "ben", "cara", "dev", "eli"]
stories = {
"ana": [NOW - 2 * H, NOW - 1 * H],
"ben": [NOW - 30 * H], # expired
"cara": [NOW - 5 * H],
"dev": [NOW - 20 * H, NOW - 10 * H, NOW - 3 * H],
}
seen = {"cara": NOW - 4 * H} # viewer already watched cara's only story
affinity = {"ana": 0.2, "cara": 0.9, "dev": 0.7}
for author, state, n in build_tray(following, stories, seen, NOW, affinity):
print(f"{author:<5} {state:<7} {n} live item(s)")
Trade-offs
- Pre-signed direct uploads remove media from the app servers but cost an extra round trip and a callback to confirm the upload.
- Generating renditions eagerly makes first loads fast but costs storage for sizes that may never be requested; lazy generation saves storage but slows first views.
- Pushing posts into feeds gives fast reads but costs write amplification; pulling stories on read avoids that because stories expire quickly.
- Aggregated like counters scale to viral posts but cost a few seconds of staleness in the displayed count.
- Erasure coding cuts media storage cost versus triple replication but costs more CPU and slower recovery when a disk fails.
What a strong answer shows
- Keeps media bytes out of the API tier with pre-signed uploads and a CDN for delivery.
- Designs an async media pipeline with multiple renditions and immutable keys.
- Gives a feed design that handles popular accounts and stores ids rather than copies.
- Separates idempotent like records from aggregated counters for hot posts.
- Uses TTLs and read-time assembly for stories rather than fan-out and cleanup jobs.
- Sizes storage and bandwidth and lets those numbers drive the design.
Mistakes to avoid
- Uploading images through the application servers and storing them in the database.
- Serving a single original size to every device, which wastes bandwidth and slows loads.
- Incrementing a like counter row synchronously on every tap.
- Fanning stories out to every follower and then needing a job to remove them 24 hours later.
- Ignoring private accounts and visibility checks on read.
Follow-ups to expect
- How would you add content moderation? (Async scanning after upload; hold the post as pending for high-risk accounts.)
- How do you delete a user's account and all media? (Tombstone, remove references, async purge from blob store and CDN.)
- How would you add an Explore page? (Candidate generation from popular and similar posts, ranked per user; separate from the follow feed.)
- How do you dedupe identical uploads? (Content hash of the original; store once, reference many times.)