Skip to content
System Design

Learn

Design Instagram

Designing Instagram: media upload and processing, feed fanout, the follow graph, stories with TTL, and serving billions of images through a CDN.

3 min readUpdated 2 Sept 2026

#Media#Fanout#CDN#Social

Instagram is news feed plus a media pipeline. The feed mechanics are shared with Twitter; what is specific here is that every post carries a large binary, and images dominate both storage and bandwidth.

Requirements

Functional

  • Upload a photo or video with a caption.
  • Follow users; see a feed of their posts.
  • Like and comment.
  • Stories that expire after 24 hours.
  • Explore / search.

Non-functional

  • Feed loads in a couple of hundred milliseconds.
  • Images load instantly — this is the product.
  • Eventual consistency is fine for feeds and counts.
500M daily users, 100M photos/day ≈ 1,200 uploads/second
Feed reads: 500M × 20 opens/day ≈ 120,000 reads/second
Storage: 100M × ~2 MB (original + renditions) ≈ 200 TB/day

Upload pipeline

Client
  → request a presigned URL
  → upload the original DIRECTLY to blob storage
  → completion event → processing queue
        ├─ generate renditions (thumbnail, feed, full)
        ├─ strip EXIF (privacy: photos carry GPS coordinates)
        ├─ transcode video, extract a poster frame
        ├─ run safety classifiers
        └─ write metadata row, then fan out to feeds

Uploading straight to object storage keeps petabytes off the application servers, as in YouTube. Stripping EXIF is worth mentioning unprompted — it is a real privacy requirement, not a detail.

Renditions matter for cost: serving a 12-megapixel original into a 400 px feed slot wastes bandwidth on every single view. Generate a small set of sizes and pick per device.

Storage

posts       post_id (snowflake), user_id, caption, media_ids, created_at
media       media_id, post_id, type, renditions{size → url}, width, height
follows     follower_id, followee_id
likes       post_id, user_id, created_at
counters    post_id → like_count, comment_count   (separate, sharded)

Media bytes live in blob storage; the database holds only URLs. Counters live apart from the post row for the same reason as in Twitter — a viral post's row would otherwise become a write hotspot.

Shard posts by post_id so a popular account's history does not concentrate on one shard.

Feed

The hybrid fanout from news feed: push post ids into followers' Redis lists for ordinary accounts, pull at read time for accounts above a follower threshold, merge the two on read. Feeds store ids only, hydrated from a post cache — so a deleted or edited post needs no rewrite across millions of lists.

Ranking is a model over affinity, recency and engagement rather than pure chronology, but the storage design is unchanged: rank the candidates the fanout produced.

Serving images

This is where the money goes.

Client → CDN edge (hit for the overwhelming majority)
           miss → origin blob storage → cache

Three things worth saying: pick the rendition by device and network conditions; use modern formats (WebP, AVIF) with fallbacks, since they are substantially smaller at the same quality; and prefetch the next few feed images while the user scrolls, so the next card is already local.

Media URLs are immutable and content-addressed, which makes them infinitely cacheable — no invalidation problem at all.

Stories

Stories look like a separate product and are mostly a TTL on existing machinery:

stories   story_id, user_id, media_id, created_at, expires_at (created + 24h)

Set a TTL in the cache and let a background job clean the storage. The tray of "who has a story" is a small per-user read across followees — cheap enough to compute on read, and naturally bounded because only the last 24 hours qualifies.

The interesting difference from posts is the viewed-by list: every view is recorded per viewer per story, which is a heavy write path for a feature that is read once by one person. Batch the writes and accept eventual consistency.

Explore is a recommendation surface: candidate generation from accounts and hashtags adjacent to the user's graph and history, then ranking. Precompute per user asynchronously and cache; it is not latency-critical enough to compute live.

Search over usernames, hashtags and captions is a separate inverted index fed from the write path, as in Twitter.

Follow-ups to expect

  • Hot post — a celebrity post read tens of millions of times; edge caching plus sharded

counters.

  • Private accounts — an authorisation check at hydration time, not at fanout, so a

privacy change takes effect immediately.

  • Feed for a new user — cold start; fall back to recommended accounts.
  • Storage cost — tier cold originals to archival storage and keep only renditions hot.
  • Content moderation — classifiers on upload, plus a human review queue for borderline

cases.

  • Video — the same transcoding and adaptive-bitrate concerns as YouTube.

Track this problem on the System Design sheet.

Frequently asked

How should photo uploads be handled at scale?

The client requests a presigned URL and uploads the original directly to object storage, so application servers never handle the bytes. A completion event triggers an async pipeline that generates renditions, strips EXIF metadata, runs safety classifiers and writes the metadata row before fanning the post out to followers' feeds.

Why generate multiple image sizes?

Bandwidth is the dominant cost. Serving a full-resolution original into a 400-pixel feed slot wastes it on every view, and views vastly outnumber uploads. Generating a small set of renditions at upload time and choosing per device and network condition cuts delivered bytes by an order of magnitude.

How are stories implemented?

As ordinary posts with an expiry timestamp — a 24-hour TTL in the cache and a background job to clean up storage. The story tray is a bounded read across the accounts a user follows, cheap because only the last day qualifies. The notable extra cost is the viewed-by list, which writes a row per viewer per story and should be batched.

Related