Skip to content

System DesignWorked problems

Design Instagram

Design Instagram. Start with one server and one database, let the numbers move bytes straight to storage, process media afterwards, serve views from a CDN and shard metadata by user, then stress it with crashing workers, a million likes, expiring stories and the storage bill, with interview prompts and code.

Mid-level28 min core · 46 min full depth

“Design Instagram.” Most candidates hear “news feed with pictures” and spend forty minutes on the feed. The interviewer usually wanted the pictures.

Instagram is three systems that share a login. A media pipeline takes a photo or video from a phone on a bad network, and turns it into versions for every screen. A serving layer sends those bytes from a CDN at terabytes a second, which is where the money goes. And a social layer, where one celebrity post can put a million likes on one row.

The feed itself is the news feed problem, so this page only sketches it. We start with one server and one database, and let the numbers break it. Then we test the finished design on what Instagram adds: bytes, counters and expiry.

If you only have a minute, this is the design. The rest of the page explains each part.

  1. Bytes never touch our servers. The phone uploads straight to object storage in 8 MB parts, so a dropped connection costs one part. A CDN serves every view.
  2. Processing happens after the upload. Workers make each size of the photo or video, then mark it ready. Every step is safe to repeat, so crashes and duplicate events never lose or double a photo.
  3. Metadata is split by user, and every ID says which shard it lives on. A user’s profile and likes are one query on one shard, and any ID finds its shard without a lookup.
  4. Hot like counts are split into slots. The like itself is the truth. The count is a fast copy that can be a few seconds behind.
  5. Stories expire on read. Every read checks the time, so a story is never shown after 24 hours, and cleanup can happen in bulk later.

Instagram is huge, so the first job is choosing what to design.

Interviewerminute 0

Design Instagram.

What they are testing

Whether you ask which features matter and how much is video, and say you will focus on media.

These four questions matter most:

  • Which features? Upload photos and short video, the feed, likes, comments, and stories.
  • How many users? 500 million a day. Some accounts have over 500 million followers, and some posts get millions of likes.
  • How much video? A quarter of uploads, about 20 seconds each. Long video is a different problem.
  • Do stories really disappear? Yes, from viewers after 24 hours. The author keeps a private archive.

It’s worth saying out loud: “I’d like to spend most of the time on uploads, storage and counters, and sketch the feed quickly. Does that match what you want?”

From these answers we make four promises:

  • Viewing always works. 99.99% for the feed, profiles and media. Uploads may wait, because the app keeps unfinished uploads and retries them.
  • Fast. Post and profile reads under 200 ms of server time. Images start arriving within 100 ms from the CDN.
  • Quick to publish. A photo is live within 5 seconds of its upload finishing, a video within 60.
  • Never lose a photo. Once an upload is accepted, it’s in two separate places before we say so.

We leave out direct messages, Reels recommendations, long video and ads.

Go deeperMore questions, and what each changes1 min
Question We assume If the answer were different
Private accounts? Yes, only approved followers see their media All public lets media URLs be plain and cached anywhere
Only 5 million users? No One region, a commercial CDN and one Postgres with replicas
Photos only? No, a quarter video Removes video processing, the largest compute cost
Exact like counts? No, a few seconds behind is fine Exact counts per like need a much slower design

Now we work out the numbers. For Instagram, bytes matter more than requests.

Assumptions
Daily active users
500 million
Photos and videos viewed per user a day
200
Peak over average
3×
Feed photos and videos uploaded a day
100 million
Story photos and videos uploaded a day
200 million
Share of uploads that are video
25%
Uploaded photo size
2 MB
Uploaded video size
10 MB
Bytes served per view, average
400 KB
Likes per user a day
8
Views1.2M/s3.5M/s at peak

500 million daily users see about 200 photos and videos each a day.

So we serve media only from a CDN.

Uploads3,500/s10,000/s at peak; 42 GB/s

100 million posts and 200 million stories a day, a quarter of them video. At 2 MB per photo and 10 MB per video, the peak is 42 GB of uploads a second.

So we upload straight to object storage, never through our servers.

Egress1.4 TB/sat peak

About 400 KB per view on average. That’s over 1,000 PB a month.

So we image sizes and the CDN hit rate are design decisions: this is the largest bill.

Storage700 TBa day; 1.3 EB in 5 years

Each photo or video plus its smaller versions. 1.3 exabytes over five years, before copies.

So we three copies of photos nobody looks at is the cost to cut.

Video work140,000CPU cores busy

75 million videos a day, 20 seconds each, at about 8 core-seconds per second of video.

So we video gets its own queue and workers, so a video backlog never delays photos.

Metadata3.5M/slookups; 290,000 writes/s

About one lookup per view. Likes alone are 4 billion a day, 140,000 a second at peak.

So we cache and shard from day one.

Try the calculator. Even at a tenth of the uploads and no video, uploads are 2.1 GB a second. So direct uploads aren’t an optimization here. They’re the only sane path.

Change an assumption
Media views, average
1,200,000/s
Media views, peak
3,500,000/s
Every view is an object fetch: the CDN takes them, not our servers.
Uploads, average
3,500/s
Uploads, peak
10,000/s
Upload ingest, peak
42 GB/s
More than a fleet of API servers should relay: clients upload straight to object storage with presigned URLs.
CDN egress, peak
1.4 TB/s
Egress is the largest bill on the page: a CDN with PoPs inside ISPs, small renditions, and hit ratio as a first-class metric.
Origin fetches at a 95% hit ratio, peak
170,000/s
Add an origin shield tier so every PoP does not miss to storage separately.
Feed media stored a day
700 TB
Original plus renditions: about 1.3× for photos, 2× for video.
Feed media over 5 years, one copy
1300 PB
Past 100 PB, keeping three full copies of cold media is the cost to cut: move it to an erasure-coded warm tier.
Live stories, 24 hours of renditions
1.4 PB
A rolling window, not growth: renditions are deleted when the story expires.
Transcoding cores, average
140,000
Video is the largest compute line: its own queue apart from photos, and full ladders only for videos people watch.
Likes, peak
140,000/s
Spread over millions of posts this is easy. One celebrity post is the hard part.

We start with the simplest design that works. Then we let the numbers break it, one at a time. Each step fixes only what broke, adds only the boxes that fix needs, and says what the fix costs. Throughout, follow one scenario: a user on the subway uploads a 50 MB video and loses signal halfway, and then a million people want to see it.

The simplest Instagram is one app server and one database. The phone sends the photo to the server, the server saves it to disk and writes a row, and the server sends the photo back to every viewer.

Instagram · architecture

Stage 1: One server, one database

UsersApp serverfiles on local disk70000% busyPostgresall metadata35000% busy
Color keyServiceDatabaseCacheQueue or streamBackground workerNew in this stageAsync or occasional

Stage 1 of 9

One server, one database

Why this stage

Nothing yet.

This is the smallest design that lets a user post a photo and others see it.

What we add

An app server that receives the upload, resizes it, writes the files to its own disk and serves them back, and one Postgres with users, posts and likes.

What it costs

Every byte of every upload and every view passes through one machine, and every like is a write on one database.

At 3.5M media views per second, API servers is 70000% busy. Stage 2 is the fix.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
The numbers behind the busy bars

How busy it gets

  • For every 300 media views, someone uploads 1 photo or video.
  • There is about one API request for every 10 views, and one feed load for every 20.
  • Each view needs about one metadata lookup, for the post around it.

What one machine can do

  • One app server serves about 5,000 small files a second and 500 uploads when it handles the bytes itself. Without the bytes, it handles about 10,000 metadata requests.
  • One Postgres shard serves about 10,000 reads a second and 8,000 writes. There are 64 shards.
  • One 64-core media worker handles about 1.6 uploads a second, because video dominates: 20 seconds of video at 8 core-seconds per second.
  • The feed service handles about 4,000 loads a second per server.

How big the caches are

  • The metadata cache is 30 Redis nodes, about 200,000 lookups a second each. It answers 95% of lookups.
  • The counter store is 20 Redis nodes.

Why writes add up

  • Each upload brings about 28 metadata writes: the post, comments, follows and about 13 likes.
  • Until stage 7, each like is an edge plus a counter update.

The numbers break this at once, in both directions. Uploads arrive at 42 GB a second at peak, and views leave at 1.4 TB a second. No fleet of app servers should carry those bytes. And a phone on the subway that loses signal halfway would start a 50 MB upload again from zero.

The phone asks our API to start an upload. The API creates a media row and hands back presigned URLs: links that let the phone write directly to Amazon S3, for a limited time, without our servers in between.

POST /v1/uploads
{ "uploadKey": "c1f0e2a4-...", "contentType": "video/mp4", "bytes": 48211904 }
201 Created
{ "mediaId": "2948102844431360001", "partSize": 8388608,
"parts": [{ "n": 1, "url": "https://upload.example/m/2948.../1?X-Sig=..." }, ...] }

The phone uploads 8 MB parts, several at once. After losing signal, it asks which parts arrived, and sends only the rest. The app creates uploadKey itself, so a retried request gets the same mediaId back. The app even starts uploading the moment the user picks a photo, while they type a caption, so most uploads finish before they press Share.

Instagram · architecture

Stage 2: Bytes go straight to storage

UsersLoad balancernewAPI serversmetadata only61 servers, 59% busyObject storageoriginals, renditionsnewPostgresall metadata35000% busy
Color keyServiceDatabaseCacheQueue or streamBackground workerNew in this stageAsync or occasional
Stage

Stage 2 of 9

Bytes go straight to storage

Why this stage

At the peak, 42 GB/s of uploads.

No fleet of API servers should relay that many bytes, and a phone that loses signal should not restart a 50 MB upload from zero.

What we add

Object storage for originals and renditions, and presigned URLs so phones upload straight to it, in parts.

API servers handle only metadata.

What it costs

The bytes and the post row live in two systems, so an upload must be tied to its post when it completes.

At 3.5M media views per second, Metadata DB is 35000% busy. Stage 3 is the fix.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
The numbers behind the busy bars

How busy it gets

  • For every 300 media views, someone uploads 1 photo or video.
  • There is about one API request for every 10 views, and one feed load for every 20.
  • Each view needs about one metadata lookup, for the post around it.

What one machine can do

  • One app server serves about 5,000 small files a second and 500 uploads when it handles the bytes itself. Without the bytes, it handles about 10,000 metadata requests.
  • One Postgres shard serves about 10,000 reads a second and 8,000 writes. There are 64 shards.
  • One 64-core media worker handles about 1.6 uploads a second, because video dominates: 20 seconds of video at 8 core-seconds per second.
  • The feed service handles about 4,000 loads a second per server.

How big the caches are

  • The metadata cache is 30 Redis nodes, about 200,000 lookups a second each. It answers 95% of lookups.
  • The counter store is 20 Redis nodes.

Why writes add up

  • Each upload brings about 28 metadata writes: the post, comments, follows and about 13 likes.
  • Until stage 7, each like is an edge plus a counter update.

With bytes going straight to storage, there’s no server in the middle to resize the photo. So the upload ends with an event. Storage announces “new object”, a media queue holds the event, and media workers check the file, make each size, and mark the media row ready. Photos get four sizes. Videos get 480p and 720p first, and 1080p a few minutes later.

A resumable upload, then processing
Metadata DBMedia workerMedia queueObject storageAPI serverPhoneMetadata DBMedia workerMedia queueObject storageAPI serverPhonePOST /uploads with upload key Kinsert media row, state uploadingmedia ID and presigned part URLsPUT parts 1 and 2part 3 lost in a tunnelwhich parts arrived?1 and 2, from the storage part listPUT part 3completecomplete multipart, row to uploadedobject created (at least once)deliverclaim, render renditions, mark readyMetadata DBMedia workerMedia queueObject storageAPI serverPhoneMetadata DBMedia workerMedia queueObject storageAPI serverPhonePOST /uploads with upload key Kinsert media row, state uploadingmedia ID and presigned part URLsPUT parts 1 and 2part 3 lost in a tunnelwhich parts arrived?1 and 2, from the storage part listPUT part 3completecomplete multipart, row to uploadedobject created (at least once)deliverclaim, render renditions, mark ready
Scroll or pinch to zoom, drag to pan
  • Color key
  • Database
  • Background worker
  • Queue or stream
  • Service

The user can share the post while the media is still processing. It waits as pending, and goes live when its last photo is ready. Meanwhile the author’s app shows it from the local file, so it feels instant. A photo takes about a second, a 20-second video about 15 seconds to be playable.

Instagram · architecture

Stage 3: Processing comes after

UsersLoad balancerAPI serversmetadata only61 servers, 59% busyObject storageoriginals, renditionsMedia queueupload eventsnewMedia workersresize, encodenew10938 servers, 60% busyPostgresall metadata35000% busy
Color keyServiceDatabaseCacheQueue or streamBackground workerNew in this stageAsync or occasional
Stage

Stage 3 of 9

Processing comes after

Why this stage

A 12 MP original is the wrong file to show on a phone, and resizing a photo or transcoding a video takes seconds to minutes, far too long for the upload request.

What we add

A media queue fed by storage events, and workers that resize photos and transcode video into renditions.

What it costs

A post appears a few seconds after the upload finishes, and every step must be safe to retry.

At 3.5M media views per second, Metadata DB is 35000% busy. Stage 4 is the fix.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
The numbers behind the busy bars

How busy it gets

  • For every 300 media views, someone uploads 1 photo or video.
  • There is about one API request for every 10 views, and one feed load for every 20.
  • Each view needs about one metadata lookup, for the post around it.

What one machine can do

  • One app server serves about 5,000 small files a second and 500 uploads when it handles the bytes itself. Without the bytes, it handles about 10,000 metadata requests.
  • One Postgres shard serves about 10,000 reads a second and 8,000 writes. There are 64 shards.
  • One 64-core media worker handles about 1.6 uploads a second, because video dominates: 20 seconds of video at 8 core-seconds per second.
  • The feed service handles about 4,000 loads a second per server.

How big the caches are

  • The metadata cache is 30 Redis nodes, about 200,000 lookups a second each. It answers 95% of lookups.
  • The counter store is 20 Redis nodes.

Why writes add up

  • Each upload brings about 28 metadata writes: the post, comments, follows and about 13 likes.
  • Until stage 7, each like is an edge plus a counter update.

Follow an upload through the design:

Instagram · architecture

Trace a request: Start an upload

UsersLoad balancerAPI serversmetadata onlyObject storageoriginals, renditionsMedia queueupload eventsMedia workersresize, encodePostgresall metadata
Color keyServiceDatabaseCacheQueue or streamBackground workerNew in this stageAsync or occasionalHop in motionHops doneResponse

Start an upload: step 1 of 7

Users to Load balancer

POST /v1/uploads with the content type, size and a client-generated upload key. The phone has already resized the photo to 1080 px and stripped location metadata if the user asked.

30 ms of a 200 ms budget
  1. Users → Load balancer30 ms
  2. Load balancer → API servers1 ms
  3. API servers → Metadata DB5 ms
  4. Users → Object storageasync
  5. Object storage → Media queueasync
  6. Media queue → Media workersasync
  7. Response
Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
Go deeperWhere the seconds go after an upload1 min
Step Photo 20 s video
Waiting in the queue at peak Under 1 s Under 5 s (its own queue)
Check the real file type, size and format 50 ms 200 ms
Safety checks (known-abuse matches, a classifier) 100 ms 1 s, on sampled frames
First sizes 300 ms, all four 10 s for 480p and 720p, pieces in parallel
Mark ready, publish the pending post 10 ms 10 ms; 1080p follows within minutes
Total About 1 s About 15 s to playable

Video is cut into short pieces that are processed in parallel, which is why a 20-second video is playable in 15 seconds instead of a minute.

Bytes no longer pass through our servers. But every view still reads object storage, 3.5 million times a second at peak. So every image and video is served by a CDN, which reads object storage only on a miss. Media URLs name one exact version, like https://cdn.example/m/{mediaId}/v3/640.webp. That file never changes, so the CDN and the phone can cache it for a year.

Instagram · architecture

Stage 4: Views come from a CDN

UsersCDNrenditions, edgenewLoad balancerAPI serversmetadata only61 servers, 59% busyObject storageoriginals, renditionsMedia queueupload eventsMedia workersresize, encode10938 servers, 60% busyPostgresall metadata35000% busy
Color keyServiceDatabaseCacheQueue or streamBackground workerNew in this stageAsync or occasional
Stage

Stage 4 of 9

Views come from a CDN

Why this stage

At the peak, 3.5 million media views a second.

One server serves about 5,000 files a second.

What we add

A CDN in front of object storage.

Media files never change, so edges and phones cache them for a year.

What it costs

A CDN bill that grows with every view, and new media that misses once at every edge.

At 3.5M media views per second, Metadata DB is 35000% busy. Stage 5 is the fix.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
The numbers behind the busy bars

How busy it gets

  • For every 300 media views, someone uploads 1 photo or video.
  • There is about one API request for every 10 views, and one feed load for every 20.
  • Each view needs about one metadata lookup, for the post around it.

What one machine can do

  • One app server serves about 5,000 small files a second and 500 uploads when it handles the bytes itself. Without the bytes, it handles about 10,000 metadata requests.
  • One Postgres shard serves about 10,000 reads a second and 8,000 writes. There are 64 shards.
  • One 64-core media worker handles about 1.6 uploads a second, because video dominates: 20 seconds of video at 8 core-seconds per second.
  • The feed service handles about 4,000 loads a second per server.

How big the caches are

  • The metadata cache is 30 Redis nodes, about 200,000 lookups a second each. It answers 95% of lookups.
  • The counter store is 20 Redis nodes.

Why writes add up

  • Each upload brings about 28 metadata writes: the post, comments, follows and about 13 likes.
  • Until stage 7, each like is an edge plus a counter update.

Step 5. Shard the metadata by user, with the shard inside the ID

Section titled “Step 5. Shard the metadata by user, with the shard inside the ID”

Metadata is the small stuff: posts, captions, likes, comments. A cache answers about 95% of lookups. Behind it, Postgres is split into 64 shards, because 290,000 writes a second is far more than one database takes. The real decision is what to split by.

Split by Post by ID Profile grid “Did I like these?” Problem
Hash of the post ID One shard Every shard Every shard Every profile view asks every shard
User ID, plus a directory for IDs Directory, then one One shard One shard A directory lookup on the busiest path
User ID, with the shard inside the ID One shard One shard One shard A user can never change shards

Instagram described this in 2012. Each ID is 64 bits: 41 bits of time, 13 bits naming the owner’s logical shard, and 10 bits of counter. So any ID tells you its shard directly, with no lookup. There are 8,192 logical shards spread over 64 servers. Growing means moving whole logical shards to new servers, never reshuffling users. And IDs still sort by time.

Likes are stored with the person who liked. So “did I like these 20 posts?” is one query on one shard. And a celebrity’s likes spread across every shard, instead of piling onto hers. A separate table of who liked each post is filled in the background.

Instagram · architecture

Stage 5: Shard the metadata

UsersCDNrenditions, edgeLoad balancerAPI serversmetadata only61 servers, 59% busyObject storageoriginals, renditionsMedia queueupload eventsMedia workersresize, encode10938 servers, 60% busyMetadata DB64 shards by user57% busyMetadata cacheposts, media, usersnew58% busy
Color keyServiceDatabaseCacheQueue or streamBackground workerNew in this stageAsync or occasional
Stage

Stage 5 of 9

Shard the metadata

Why this stage

Each view needs about one metadata lookup, 3.5 million a second at peak, and one Postgres serves about 10,000.

Writes are near 290,000 a second.

What we add

A metadata cache in front, and Postgres split into 64 shards by user ID.

Every post and media ID carries its owner’s shard number, so a lookup by ID goes straight to the right shard.

What it costs

A user lives on one shard for good, and anything not keyed by user (likers of a post) needs its own table.

At 3.5M media views per second, every part has headroom. Raise the traffic until something turns red.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
The numbers behind the busy bars

How busy it gets

  • For every 300 media views, someone uploads 1 photo or video.
  • There is about one API request for every 10 views, and one feed load for every 20.
  • Each view needs about one metadata lookup, for the post around it.

What one machine can do

  • One app server serves about 5,000 small files a second and 500 uploads when it handles the bytes itself. Without the bytes, it handles about 10,000 metadata requests.
  • One Postgres shard serves about 10,000 reads a second and 8,000 writes. There are 64 shards.
  • One 64-core media worker handles about 1.6 uploads a second, because video dominates: 20 seconds of video at 8 core-seconds per second.
  • The feed service handles about 4,000 loads a second per server.

How big the caches are

  • The metadata cache is 30 Redis nodes, about 200,000 lookups a second each. It answers 95% of lookups.
  • The counter store is 20 Redis nodes.

Why writes add up

  • Each upload brings about 28 metadata writes: the post, comments, follows and about 13 likes.
  • Until stage 7, each like is an edge plus a counter update.

This is enough for a mid-level answer. Direct uploads, processing after, a CDN, and metadata sharded by user. The stress tests are what make it a senior answer.

Go deeperMoving a logical shard to a new server1 min

With 8,192 logical shards on 64 servers, each server holds 128 of them. Each logical shard is its own Postgres schema, so it can be copied as one unit. Growing to 128 servers means moving half of each server’s logical shards to a new machine.

A move has four steps. First, the new server starts as a live copy of the old one, using the database’s own replication. Second, once it has caught up, writes to the moving shards pause for a second or two. Third, the small map of “logical shard to server” is updated, and every API server picks up the change. Fourth, writes resume on the new server, and the old copies are dropped a day later.

No ID changes, and no user moves between logical shards, because the shard number is inside every ID. That is the payoff of putting it there.

Go deeperThe feed and the follow graph, in brief2 min

The feed is the news feed design, unchanged. In short, a worker copies each new post ID into every follower’s timeline, a short list in Redis. Accounts with more than about a million followers are skipped, and their posts are merged in when the feed is read. The one change: copying starts only when the post leaves pending, so nobody sees a post whose photo is still processing.

The follow graph is two tables: who follows me, on my shard, and who I follow, on yours. Both are one-hop questions, which plain sharded SQL answers well.

Instagram · architecture

Stage 6: Follows and the feed

UsersCDNrenditions, edgeLoad balancerAPI serversmetadata only31 servers, 60% busyObject storageoriginals, renditionsMedia queueupload eventsMedia workersresize, encode10938 servers, 60% busyMetadata DB64 shards by user57% busyGraph storefollows, both waysnewFeed servicesee news feednew73 servers, 60% busyMetadata cacheposts, media, users58% busy
Color keyServiceDatabaseCacheQueue or streamBackground workerNew in this stageAsync or occasional
Stage

Stage 6 of 9

Follows and the feed

Why this stage

The product requirement: a home feed of posts from accounts you follow.

Not the slider, which stays green.

What we add

A graph store holding follow edges in both directions, and a feed service with hybrid fan-out, the design from the news feed page.

What it costs

A follow writes two rows on two shards without a transaction, so a repair job reconciles them.

At 3.5M media views per second, every part has headroom. Raise the traffic until something turns red.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
The numbers behind the busy bars

How busy it gets

  • For every 300 media views, someone uploads 1 photo or video.
  • There is about one API request for every 10 views, and one feed load for every 20.
  • Each view needs about one metadata lookup, for the post around it.

What one machine can do

  • One app server serves about 5,000 small files a second and 500 uploads when it handles the bytes itself. Without the bytes, it handles about 10,000 metadata requests.
  • One Postgres shard serves about 10,000 reads a second and 8,000 writes. There are 64 shards.
  • One 64-core media worker handles about 1.6 uploads a second, because video dominates: 20 seconds of video at 8 core-seconds per second.
  • The feed service handles about 4,000 loads a second per server.

How big the caches are

  • The metadata cache is 30 Redis nodes, about 200,000 lookups a second each. It answers 95% of lookups.
  • The counter store is 20 Redis nodes.

Why writes add up

  • Each upload brings about 28 metadata writes: the post, comments, follows and about 13 likes.
  • Until stage 7, each like is an edge plus a counter update.
Option Fits because Costs
Sharded SQL, both directions One-hop scans; exact counts; no duplicate edges Two writes per follow, on two shards
A wide-column store Huge follower lists page well Weaker uniqueness and counts
A graph database Questions many hops deep Instagram asks one-hop questions over billions of edges

A follow is two writes on two shards with no shared transaction, so a repair job fixes any pair that got half-written. A list of 600 million followers is about 12 GB on one shard, and the feed never reads it, because such accounts are merged in at read time instead. “Friends of friends” questions are answered by an offline job.

Go deeperThe rest of the API, and where things are stored2 min
POST /v1/uploads/{mediaId}/complete finish the upload; row moves to "uploaded"
POST /v1/posts Idempotency-Key, { caption, mediaIds } → 201 pending
PUT /v1/posts/{postId}/likes/me 204; liking twice is liking once
DELETE /v1/posts/{postId}/likes/me 204
POST /v1/posts/{postId}/comments Idempotency-Key, { text } → 201
POST /v1/stories { mediaId } → 201 { storyId, expiresAt }
GET /v1/stories/tray people you follow with a live story

complete is done by our API, not the phone. It finishes the upload with storage and moves the row from uploading to uploaded with a conditional update, so a retried complete finds it already moved and gets the same answer.

media (media_id BIGINT PRIMARY KEY, owner_id BIGINT, kind TEXT,
state TEXT, -- uploading | uploaded | processing | ready | failed
upload_key UUID UNIQUE, -- a retried create finds the same row
renditions JSONB, width INT, height INT,
lease_owner TEXT, lease_until TIMESTAMPTZ, attempts INT, version BIGINT)
likes (user_id BIGINT, post_id BIGINT, PRIMARY KEY (user_id, post_id)) -- on the liker's shard
comments (post_id BIGINT, comment_id BIGINT, author_id BIGINT, text TEXT,
PRIMARY KEY (post_id, comment_id)) -- on the owner's shard

Photos never go in the database. A row holds storage keys, and what the app needs to lay out a post before the bytes arrive: size, length, which versions exist.

The design is complete. Now we test it where Instagram is hard: workers that crash, a celebrity post, stories that must vanish on time, and the bill for bytes. The last three add a small new piece each.

An upload pipeline that survives everything

Section titled “An upload pipeline that survives everything”
Interviewerminute 20

Your queue delivers at least once and workers crash. Convince me no photo is lost, processed twice, or stuck forever.

What they are testing

Whether no photo is lost, processed twice in a harmful way, or stuck forever when workers crash.

Events arrive at least once, so the same “new photo” event can come twice. Workers crash halfway. So the worker treats the media row as the truth, and the event only as a hint to go look. Then it makes every step safe to repeat:

  • Claim with a lease. A conditional update moves the row to processing, with this worker as owner, a 2-minute lease, and the row’s version number plus one. Two workers can’t both succeed. A long video job renews its lease as it goes.
  • Predictable output names. Each file’s name comes from the media ID, the pipeline version and the size. So a retry sees what an earlier attempt finished, and skips it.
  • Fence the finish. “Mark ready” only succeeds if the row still has this worker’s version. A worker that froze past its lease changes nothing when it wakes up.
  • A retry limit. After 5 tries the file is marked failed, for example a video that crashes the decoder, and the author is asked to try again. It never blocks the queue.
  • A sweeper. A job finds rows stuck in uploaded or processing past their lease, and queues them again. A lost event delays a photo, but can’t lose it.
MediaWorker.javamedia_worker.pyMediaWorker.csmedia_worker.gomedia-worker.ts
// The media row is the truth; a queue message only says "look at media X". Delivery is at
// least once, so this handler can run twice, at once, or after a crash halfway through.
enum State { UPLOADING, UPLOADED, PROCESSING, READY, FAILED }
record Media(String id, State state, String originalKey, String leaseOwner, long leaseUntil,
int attempts, long version) {}
record Rendition(String name, int maxEdge, String format) {}
interface MediaRows {
Media get(String id);
// Conditional updates: each runs only if the row still has the version we read, so two
// workers can never both believe they own the job.
boolean claim(String id, long version, String owner, long leaseUntil); // attempts + 1
boolean markReady(String id, long version, List<String> renditionKeys);
boolean markFailed(String id, long version);
}
interface Blobs {
boolean exists(String key);
byte[] get(String key);
void put(String key, byte[] bytes);
}
interface Renderer { byte[] render(byte[] original, Rendition r); }
class MediaWorker {
// In every output key, so a new pipeline version writes new files and never overwrites
// what the CDN is already serving under the old keys.
static final int PIPELINE_VERSION = 3;
static final long LEASE_MS = 120_000; // longer than any job; a dead worker's claim expires
static final int MAX_ATTEMPTS = 5; // after that the input is poison, not unlucky
static final List<Rendition> LADDER = List.of(
new Rendition("1080", 1080, "webp"), new Rendition("640", 640, "webp"),
new Rendition("320", 320, "webp"), new Rendition("thumb", 150, "jpg"));
private final MediaRows rows;
private final Blobs blobs;
private final Renderer renderer;
private final String workerId;
private final LongSupplier clock; // injected so tests can expire leases
MediaWorker(MediaRows rows, Blobs blobs, Renderer renderer, String workerId, LongSupplier clock) {
this.rows = rows;
this.blobs = blobs;
this.renderer = renderer;
this.workerId = workerId;
this.clock = clock;
}
static String keyOf(String mediaId, Rendition r) {
return "media/" + mediaId + "/v" + PIPELINE_VERSION + "/" + r.name() + "." + r.format();
}
/** True: ack the message. False: leave it, and the queue redelivers it later. */
boolean handle(String mediaId) {
Media m = rows.get(mediaId);
if (m == null || m.state() == State.READY || m.state() == State.FAILED)
return true; // deleted, or a duplicate of a finished job
long now = clock.getAsLong();
if (m.state() == State.PROCESSING && m.leaseUntil() > now)
return false; // someone holds a live lease; retry in case they die
if (m.attempts() >= MAX_ATTEMPTS) {
rows.markFailed(m.id(), m.version()); // the app tells the author to try again
return true;
}
if (!rows.claim(m.id(), m.version(), workerId, now + LEASE_MS))
return false; // another worker claimed it between our read and now
long mine = m.version() + 1;
List<String> keys = new ArrayList<>();
byte[] original = null; // fetched lazily: a fully rendered retry never reads it
for (Rendition r : LADDER) {
String key = keyOf(m.id(), r);
keys.add(key);
if (blobs.exists(key)) continue; // an earlier attempt got this far before crashing
if (original == null) original = blobs.get(m.originalKey());
// Same input, same key: if two workers race here, either write is a valid result.
blobs.put(key, renderer.render(original, r));
}
// Fenced by version: a worker that stalled past its lease (a long GC pause) finds the
// row moved on and changes nothing. Either way the message is done.
rows.markReady(m.id(), mine, keys);
return true;
}
}
A worker stalls, another takes over, the stale one is fenced off
Worker 2Object storageMedia rowWorker 1Worker 2Object storageMedia rowWorker 1long pause, lease expiresclaim at version 7, lease 2 minwrite 1080.webp and 640.webprow is processing, lease expired, claim at version 81080 and 640 exist, skip themwrite 320.webp and thumb.jpgmark ready at version 9mark ready at version 8rejected, the row moved onWorker 2Object storageMedia rowWorker 1Worker 2Object storageMedia rowWorker 1long pause, lease expiresclaim at version 7, lease 2 minwrite 1080.webp and 640.webprow is processing, lease expired, claim at version 81080 and 640 exist, skip themwrite 320.webp and thumb.jpgmark ready at version 9mark ready at version 8rejected, the row moved on
Scroll or pinch to zoom, drag to pan
  • Color key
  • Background worker
  • Database
  • Service

When the last photo of a pending post becomes ready, a listener flips the post to live with a conditional update, and starts the feed copy. A duplicate event finds it already live and stops.

Go deeperOther ways to upload, and what a dropped connection costs1 min
Upload method A dropped connection costs Bytes through our servers Fits
One POST to our API The whole file All of them A small app with small photos
One presigned PUT The whole file None Photos on good networks
Presigned parts, 8 MB each One part None Everything here
A resumable protocol (tus) Bytes since the last offset None, if storage supports it Stores that support it natively

Uploads that are abandoned halfway are cleaned up automatically by a storage rule after a day.

A footballer with 600 million followers posts a photo. In the first minute it gets a million likes. What happens to the database?

Total like traffic is fine: 140,000 a second spread over millions of posts. The problem is one post at 20,000 a second. If each like runs UPDATE posts SET likes = likes + 1, every update to that row waits for the one before it. One row manages about a thousand a second.

So the count moves to Redis, split into slots: likes:<postId>:<slot>. A like adds one to a random slot. A read adds up the slots. Most posts have one slot. Once a post passes a few hundred likes a second, it’s raised to 64 slots, so 20,000 likes a second become about 300 per slot, spread across many machines. Each API server caches the total for 2 seconds, so the hot read is one batched read per server, not one per viewer.

Instagram · architecture

Stage 7: Hot counters

UsersCDNrenditions, edgeLoad balancerAPI serversmetadata only31 servers, 60% busyObject storageoriginals, renditionsMedia queueupload eventsMedia workersresize, encode10938 servers, 60% busyMetadata DB64 shards by user33% busyGraph storefollows, both waysFeed servicesee news feed73 servers, 60% busyMetadata cacheposts, media, users58% busyLike counterssharded per postnew18% busy
Color keyServiceDatabaseCacheQueue or streamBackground workerNew in this stageAsync or occasional
Stage

Stage 7 of 9

Hot counters

Why this stage

Not the slider: a burst.

A celebrity post can take 20,000 likes a second, all incrementing one row, and one Postgres row takes about 1,000 contended updates a second.

What we add

Like and comment counts move to a counter store, split into up to 64 slots per hot post.

The like edge stays in the liker’s shard and decides whether a like counts.

What it costs

Counts are approximate for a few seconds, and a crash between the edge and the counter can drift a count until reconciliation.

At 3.5M media views per second, every part has headroom. Raise the traffic until something turns red.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
The numbers behind the busy bars

How busy it gets

  • For every 300 media views, someone uploads 1 photo or video.
  • There is about one API request for every 10 views, and one feed load for every 20.
  • Each view needs about one metadata lookup, for the post around it.

What one machine can do

  • One app server serves about 5,000 small files a second and 500 uploads when it handles the bytes itself. Without the bytes, it handles about 10,000 metadata requests.
  • One Postgres shard serves about 10,000 reads a second and 8,000 writes. There are 64 shards.
  • One 64-core media worker handles about 1.6 uploads a second, because video dominates: 20 seconds of video at 8 core-seconds per second.
  • The feed service handles about 4,000 loads a second per server.

How big the caches are

  • The metadata cache is 30 Redis nodes, about 200,000 lookups a second each. It answers 95% of lookups.
  • The counter store is 20 Redis nodes.

Why writes add up

  • Each upload brings about 28 metadata writes: the post, comments, follows and about 13 likes.
  • Until stage 7, each like is an edge plus a counter update.

The like decides, the counter follows. The API inserts the like row with “do nothing if it already exists”, and adds to the counter only if a row was actually created. So a retry or a double tap adds nothing.

LikeCounter.javalike_counter.pyLikeCounter.cslike_counter.golike-counter.ts
// The like edge is the truth; the counter is a fast, approximate view of how many edges exist.
interface Likes {
// INSERT ... ON CONFLICT DO NOTHING on the liker's shard: true only if a row was created.
boolean insertIfAbsent(long userId, long postId);
boolean deleteIfPresent(long userId, long postId);
}
interface CounterStore { // Redis: INCRBY, and GETs pipelined per node
void incrBy(String key, long delta);
List<Long> getAll(List<String> keys); // a missing key reads as 0
}
interface HotPosts {
// 1 for almost every post. A detector raises it (up to 64) when a post takes more than a
// few hundred likes a second, and only lowers it after the post has been cold for a day,
// so readers never stop summing a slot that still holds likes.
int slots(long postId);
}
class LikeCounter {
static final long READ_TTL_MS = 2_000; // how stale a displayed count may be
private record Cached(long value, long expiresAt) {}
private final Likes likes;
private final CounterStore store;
private final HotPosts hot;
private final LongSupplier clock; // injected so tests control the cache
private final Map<Long, Cached> local = new ConcurrentHashMap<>();
LikeCounter(Likes likes, CounterStore store, HotPosts hot, LongSupplier clock) {
this.likes = likes;
this.store = store;
this.hot = hot;
this.clock = clock;
}
// No {hash tag} in the key on purpose: Redis Cluster would put every slot of a post on
// the same node, which is exactly the hot spot the slots exist to spread.
static String key(long postId, int slot) { return "likes:" + postId + ":" + slot; }
void like(long userId, long postId) {
// The edge decides whether this like counts, so a retry or a double tap adds nothing.
// A crash between the two writes loses one increment; reconciliation recounts edges.
if (likes.insertIfAbsent(userId, postId)) add(postId, 1);
}
void unlike(long userId, long postId) {
if (likes.deleteIfPresent(userId, postId)) add(postId, -1);
}
private void add(long postId, long delta) {
// A random slot spreads one post's writes over many keys, and so over many nodes:
// 20,000 likes a second become about 300 per slot with 64 slots.
int slot = ThreadLocalRandom.current().nextInt(hot.slots(postId));
store.incrBy(key(postId, slot), delta);
}
long count(long postId) {
// A hot post is read far more than it is liked: one batched read per server every
// 2 seconds, not one per viewer. Two servers may show counts a second apart.
long now = clock.getAsLong();
Cached c = local.get(postId);
if (c != null && c.expiresAt() > now) return c.value();
List<String> keys = new ArrayList<>();
for (int s = 0; s < hot.slots(postId); s++) keys.add(key(postId, s));
long sum = 0;
for (long v : store.getAll(keys)) sum += v;
sum = Math.max(0, sum); // an unlike can land on a slot before its like does
local.put(postId, new Cached(sum, now + READ_TTL_MS));
return sum;
}
}

A crash between the two writes can lose a few counts. So an hourly job recounts hot posts from the like rows and corrects the slots. Counts above 10,000 are shown rounded (“1.2M”), which hides that small drift. Follow a like, and a view of the post:

Instagram · architecture

Trace a request: Start an upload

UsersLoad balancerAPI serversmetadata onlyObject storageoriginals, renditionsMedia queueupload eventsMedia workersresize, encodePostgresall metadata
Color keyServiceDatabaseCacheQueue or streamBackground workerNew in this stageAsync or occasionalHop in motionHops doneResponse

Start an upload: step 1 of 7

Users to Load balancer

POST /v1/uploads with the content type, size and a client-generated upload key. The phone has already resized the photo to 1080 px and stripped location metadata if the user asked.

30 ms of a 200 ms budget
  1. Users → Load balancer30 ms
  2. Load balancer → API servers1 ms
  3. API servers → Metadata DB5 ms
  4. Users → Object storageasync
  5. Object storage → Media queueasync
  6. Media queue → Media workersasync
  7. Response
Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
Go deeperOther ways to count a hot post1 min
Approach Writes on the hot key Count freshness Breaks on
One counter row per post 1, one at a time Exact, immediate About a thousand likes a second
Split counter, N slots 1 on a random slot About 2 seconds Reads must add N keys
Log, then add up per second 1 per batch Seconds to a minute A stuck consumer freezes counts
Count the likes on read 0 Exact Counting 10 million rows on every view

Why not just one Redis key? It could take 20,000 increments a second. But it would pin the post to one core on one machine that also serves other keys, and a post ten times more viral wouldn’t fit. Billing by likes would need the log design with exact accounting.

Stories disappear after 24 hours. How does that happen for 200 million stories a day?

Two things are hard. The tray, the row of circles at the top of the app, loads on every app open, about 90,000 times a second. It shows which of up to 7,500 people you follow have a story right now. And expiry must be exact for viewers, but cheap for storage.

The tray is built when you open the app. Each API server keeps an in-memory map from each author to their newest live story time. That’s about 100 million authors at 16 bytes, under 2 GB, kept current from the story event stream. A tray load matches your following list against it, and ranks by how often you watch each author. Pushing every story into hundreds of trays, only to expire a day later, would cost far more.

Instagram · architecture

Stage 8: Stories that expire

UsersCDNrenditions, edgeLoad balancerAPI serversmetadata only31 servers, 60% busyObject storageoriginals, renditionsMedia queueupload eventsMedia workersresize, encode10938 servers, 60% busyMetadata DB64 shards by user33% busyGraph storefollows, both waysStories storebucketed by hournew35% busyFeed servicesee news feed73 servers, 60% busyMetadata cacheposts, media, users58% busyLike counterssharded per post18% busy
Color keyServiceDatabaseCacheQueue or streamBackground workerNew in this stageAsync or occasional
Stage

Stage 8 of 9

Stories that expire

Why this stage

The product requirement: stories disappear after 24 hours, and the tray at the top of the app must load with the app.

What we add

A stories store bucketed by hour, an in-memory index of who has an active story, and lifecycle rules that delete story renditions after expiry.

What it costs

Expiry is enforced at read time, so every story read checks a timestamp, and highlights must pin their media before the cleanup reaches it.

At 3.5M media views per second, every part has headroom. Raise the traffic until something turns red.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
The numbers behind the busy bars

How busy it gets

  • For every 300 media views, someone uploads 1 photo or video.
  • There is about one API request for every 10 views, and one feed load for every 20.
  • Each view needs about one metadata lookup, for the post around it.

What one machine can do

  • One app server serves about 5,000 small files a second and 500 uploads when it handles the bytes itself. Without the bytes, it handles about 10,000 metadata requests.
  • One Postgres shard serves about 10,000 reads a second and 8,000 writes. There are 64 shards.
  • One 64-core media worker handles about 1.6 uploads a second, because video dominates: 20 seconds of video at 8 core-seconds per second.
  • The feed service handles about 4,000 loads a second per server.

How big the caches are

  • The metadata cache is 30 Redis nodes, about 200,000 lookups a second each. It answers 95% of lookups.
  • The counter store is 20 Redis nodes.

Why writes add up

  • Each upload brings about 28 metadata writes: the post, comments, follows and about 13 likes.
  • Until stage 7, each like is an edge plus a counter update.

Expiry is checked on every read. Every read filters on expires_at with the server’s clock. So a story is never shown after 24 hours, even if cleanup is days late. Cleanup then happens in bulk. Story rows are stored in hourly buckets, and a bucket older than 25 hours is dropped whole, far cheaper than 8 million single deletes. Story files live under a daily folder that a storage rule deletes two days later.

Caches must expire too. Story files are cached only for the time left until expires_at, not the year feed photos get, and their signed links expire at the same moment. So neither a CDN copy nor a copied link outlives the story. Highlights, stories the author pins to their profile, are copied to permanent storage when pinned, so the bulk cleanup never has to check.

Go deeperOther ways to expire a story1 min
How a story expires Exact for viewers Storage cost Problem
A delete job scheduled per story Only if the job runs 200 million timers a day A missed job shows a story forever
A database expiry per row Yes, if reads check it A delete marker per row Doesn’t handle highlights
Check on read, drop hourly buckets Yes, always One bucket drop an hour Expired stories sit on disk a while

A user deleting their own story takes a separate, immediate path: mark the row deleted and purge the CDN.

The CDN invoice is the biggest line in the budget, and storage grows every month. Where does the money go?

Bytes served come first. The app asks for the width it will actually draw: 320 pixels in a profile grid, not 1080, which is a tenth of the bytes. Newer formats, WebP and then AVIF, save a quarter to a third against JPEG. Videos start at low quality, and only the first seconds are preloaded, because most videos are never finished. At 1,000 PB a month, making the average image 10% smaller saves about 100 PB a month.

Storage comes next. Views of a photo bunch up in its first days. A year later, most photos are rarely seen. Yet three copies of 1.3 exabytes is about 3.9 exabytes of disk. So older media moves to a warm tier that uses erasure coding: the data is split into 10 blocks plus 4 extra parity blocks, and any 10 of the 14 can rebuild it. That survives 4 losses, and costs 1.4 times the data instead of 3. Facebook’s f4 system did this for older photos.

Instagram · architecture

Stage 9: Cold media

UsersCDNrenditions, edgeLoad balancerAPI serversmetadata only31 servers, 60% busyObject storagehot tier, replicatedWarm storageerasure codednewMedia queueupload eventsMedia workersresize, encode10938 servers, 60% busyMetadata DB64 shards by user33% busyGraph storefollows, both waysStories storebucketed by hour35% busyFeed servicesee news feed73 servers, 60% busyMetadata cacheposts, media, users58% busyLike counterssharded per post18% busy
Color keyServiceDatabaseCacheQueue or streamBackground workerNew in this stageAsync or occasional
Stage

Stage 9 of 9

Cold media

Why this stage

Not the slider: the bill.

Feed media grows by about 700 TB a day, 1,300 PB over five years, and most of it is rarely viewed after its first month.

What we add

A warm storage tier with erasure coding.

Media older than 90 days that is rarely read moves there; hot media stays replicated.

What it costs

A view of old media that misses the CDN is slower, about 100 ms more, and the tiering job is one more thing that can lose data if it is wrong.

At 3.5M media views per second, every part has headroom. Raise the traffic until something turns red.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
The numbers behind the busy bars

How busy it gets

  • For every 300 media views, someone uploads 1 photo or video.
  • There is about one API request for every 10 views, and one feed load for every 20.
  • Each view needs about one metadata lookup, for the post around it.

What one machine can do

  • One app server serves about 5,000 small files a second and 500 uploads when it handles the bytes itself. Without the bytes, it handles about 10,000 metadata requests.
  • One Postgres shard serves about 10,000 reads a second and 8,000 writes. There are 64 shards.
  • One 64-core media worker handles about 1.6 uploads a second, because video dominates: 20 seconds of video at 8 core-seconds per second.
  • The feed service handles about 4,000 loads a second per server.

How big the caches are

  • The metadata cache is 30 Redis nodes, about 200,000 lookups a second each. It answers 95% of lookups.
  • The counter store is 20 Redis nodes.

Why writes add up

  • Each upload brings about 28 metadata writes: the post, comments, follows and about 13 likes.
  • Until stage 7, each like is an edge plus a counter update.

A tiering job moves media older than 90 days that is rarely viewed, and moves it back if it turns popular. It deletes the hot copy only after the coded copy is read back and checked. By year five, that saves about 1.2 exabytes of disk.

Go deeperThe three storage tiers, and the cache levers1 min
Tier Copies of the data Read time on a miss Holds
Hot, copied 3 About 30 ms New and popular media, live stories
Warm, erasure coded About 2.1 About 100 ms Older media that is rarely viewed
Archive About 1.2 Minutes to hours Story archives, holds for deleted accounts

Hit rate. Every point of CDN hit rate at 3.5 million views a second is 35,000 fewer reads from storage. Unchanging URLs cached for a year, plus a shared cache layer in each region, keep it high. At this volume, the cheapest bandwidth is cache boxes placed inside internet providers’ own networks.

Private media. A shared cache can’t ask who you are. So private media links carry an expiry and a signature that the edge checks before serving its cached copy. The cache ignores the signature when matching, so viewers share one copy. A copied link dies at its expiry.

Interviewerminute 40

The media workers fall an hour behind on a busy evening. What do users see?

What they are testing

Whether uploads still succeed, posts wait instead of failing, and nothing is lost.

The processing pipeline backs up. Uploads still succeed, because the bytes go straight to storage and the media row is already written. Every “new object” event waits in the media queue, so nothing is lost. Posts stay pending and go live when their last version is ready, so followers see them late. The author’s app shows the post from the local file, so the author barely notices. Workers are added as the oldest event in the queue gets older, and the slowest work, the 1080p video versions, can wait until the backlog clears.

Object storage is down. Viewing survives almost anything. About 95% of views are CDN hits, and on a miss the CDN switches to the copy in another region. Uploads wait instead of failing: the app keeps them in its outbox and resumes the parts later. Nothing posted is lost.

Click any box to break it and see what users would notice:

Instagram · architecture

Break it: what fails, who notices, what users see

UsersCDNrenditions, edgeLoad balancerAPI serversmetadata onlyObject storagehot tier, replicatedWarm storageerasure codedMedia queueupload eventsMedia workersresize, encodeMetadata DB64 shards by userGraph storefollows, both waysStories storebucketed by hourFeed servicesee news feedMetadata cacheposts, media, usersLike counterssharded per post
Color keyServiceDatabaseCacheQueue or streamBackground workerNew in this stageAsync or occasional
Break

Failure: click any box to break it

Object storage goes down

What breaks

Uploads cannot start or finish in one region, and CDN misses fail there.

How we notice

Upload completion rate and origin error rate.

What users see

New posts wait in the app’s outbox.

Viewing still works for anything at the edge, which is 95% of views.

What we do

The app resumes the multipart upload when storage returns.

Originals are replicated to a second region within minutes, and the CDN fails over to it.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.

Losing data matters most, so it’s defended in layers. Originals are in two separate places before the upload is accepted, and in a second region within minutes. Tiering checks before it deletes. And deleted media rows are kept for 30 days, so a buggy delete can be undone.

Under stress, shed in order: Explore and search first, then fresh counts, then high video quality, then tray ranking. Viewing posts and accepting uploads go last.

At ten times the load, the cache, shards, counters and stories store simply grow. The real changes are in bytes and compute. Egress reaches 14 TB a second, so the edge moves inside internet providers. Video work passes a million cores, so encoding moves to special hardware, and videos get just two sizes at upload, with the rest made once they earn views.

Go deeperEvery failure, and what users see2 min
Part Failure What users see What we do
Object storage Region down Uploads wait; 95% of views still hit the CDN Resume parts later; CDN uses the other region
Media workers Crash, bad file Slower processing; a bad file fails alone Leases, predictable names, 5 tries
Media queue Down or behind Posts stay pending longer The sweeper queues from the media table
Metadata shard Primary down 1.5% of users can’t write for about 30 s Replica takes over; retries are safe
Counter store Down Counts frozen or hidden; likes still work Buffer and replay; recount from the like rows
Stories store Down Tray empty or stale; feed fine Replica takes over; expiry still checked on read
CDN location Down Images slow in one area DNS steering; a second CDN always in use
Whole region Outage API moves in 1 to 2 min; images keep loading Promote that region’s shards elsewhere

What users can see. An author always sees their own post at once, because their profile reads the main copy for a minute after posting. Followers see it 1 to 5 seconds later. Whether you liked a post is exact, so the heart never flips back. The count can be 2 seconds behind.

Go deeperRunning it: alerts1 min

Wake someone up when: fewer than 99% of uploads go live within 15 minutes, per platform; photo processing takes more than 10 seconds, or video more than 120; the apps report media load failures above 0.1% or slow first bytes on any CDN location; the CDN byte hit rate falls below 93%, which is a bill before it’s an outage; or post reads pass 200 ms.

Here is everything we built, in the order we built it. Use the buttons to step through it.

Instagram · architecture

The full design, stage by stage

UsersCDNrenditions, edgeLoad balancerAPI serversmetadata only31 servers, 60% busyObject storagehot tier, replicatedWarm storageerasure codednewMedia queueupload eventsMedia workersresize, encode10938 servers, 60% busyMetadata DB64 shards by user33% busyGraph storefollows, both waysStories storebucketed by hour35% busyFeed servicesee news feed73 servers, 60% busyMetadata cacheposts, media, users58% busyLike counterssharded per post18% busy
Color keyServiceDatabaseCacheQueue or streamBackground workerNew in this stageAsync or occasional
Stage

Stage 9 of 9

Cold media

Why this stage

Not the slider: the bill.

Feed media grows by about 700 TB a day, 1,300 PB over five years, and most of it is rarely viewed after its first month.

What we add

A warm storage tier with erasure coding.

Media older than 90 days that is rarely read moves there; hot media stays replicated.

What it costs

A view of old media that misses the CDN is slower, about 100 ms more, and the tiering job is one more thing that can lose data if it is wrong.

At 3.5M media views per second, every part has headroom. Raise the traffic until something turns red.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.The whole design. Step through it: one server, bytes to storage, processing, a CDN, sharded metadata, the feed, hot counters, stories, then cold media.
The numbers behind the busy bars

How busy it gets

  • For every 300 media views, someone uploads 1 photo or video.
  • There is about one API request for every 10 views, and one feed load for every 20.
  • Each view needs about one metadata lookup, for the post around it.

What one machine can do

  • One app server serves about 5,000 small files a second and 500 uploads when it handles the bytes itself. Without the bytes, it handles about 10,000 metadata requests.
  • One Postgres shard serves about 10,000 reads a second and 8,000 writes. There are 64 shards.
  • One 64-core media worker handles about 1.6 uploads a second, because video dominates: 20 seconds of video at 8 core-seconds per second.
  • The feed service handles about 4,000 loads a second per server.

How big the caches are

  • The metadata cache is 30 Redis nodes, about 200,000 lookups a second each. It answers 95% of lookups.
  • The counter store is 20 Redis nodes.

Why writes add up

  • Each upload brings about 28 metadata writes: the post, comments, follows and about 13 likes.
  • Until stage 7, each like is an edge plus a counter update.
Decision We chose We gave up We’d choose differently if
Uploads Presigned parts straight to storage Checking files before the bytes arrive Uploads were tiny and rare
Processing After the upload, every step safe to repeat A post exists before its media Users could wait on a spinner
Metadata Postgres by user, the shard inside every ID A user can never change shards Most queries were by post, not user
Like counts Split Redis counter, the like row is the truth Exact counts at every instant Likes were billed
Stories Checked on read, cleaned up in hourly buckets Expired bytes sit on disk a while Stories were permanent
Old media Erasure-coded warm tier Slower reads on a rare miss Storage were cheap next to engineering time
The feed The news feed design, starting after ready Nothing new The interviewer wanted the feed (go there)
  • 0 to 4. Scope: which features, how much video, and say you’ll focus on media.
  • 4 to 9. 42 GB/s in, 1.4 TB/s out, 700 TB a day, 290,000 metadata writes a second.
  • 9 to 20. Build it: one server, then direct uploads, processing after, the CDN, sharding by user with the shard in the ID, the feed in two sentences. A mid-level answer can stop here.
  • 20 to 29. The stress tests: the upload pipeline under crashes, then the hot like counter.
  • 29 to 40. Stories, then the bill for bytes, as the interviewer leans.
  • 40 to 45. Storage going down, then your own questions.

Never skip direct uploads, a pipeline that’s safe to repeat, or the hot counter.

Interviewer asksA user deletes a post. How fast is it gone?

From the API on the next read, because the row is marked deleted. The CDN is purged within minutes. The files are removed later by a cleanup job, and kept for 30 days in case the delete was a bug.

Interviewer asksHow would you add a filter preview before upload?

On the phone. Filters run on the device’s graphics chip on the local photo, and the phone uploads the finished image. The server never renders previews, which would cost a round trip per swipe.

Go deeperMore follow-up questions1 min

A user says their photo is stuck on ‘processing’. Where do you look? The media row’s state and attempts. Stuck in uploaded means the event was lost, and the sweeper will pick it up. Stuck in processing past its lease means a worker died, and the sweeper also covers it. failed after 5 tries means a file the decoder can’t read, and the app asks the user to retry.

I post and refresh my profile. Why can’t I see my post? You should always see it: for a minute after you post, your own profile reads from your shard’s main copy, not a replica that may be a second behind. This is called reading your own writes. Your followers read from replicas and the cache, and see the post a second or two later, which nobody notices.

Comments on a viral post: newest first, or top first? Both, as two views over the same rows on the post owner’s shard. Newest is a range read by comment ID, which already sorts by time. Top comments are ranked by a background job every few minutes and cached, because exact ranking on every read isn’t worth the cost.

How do you stop people from scraping private photos? Private media links are signed and expire, so a copied link dies within a day. The API checks that the viewer follows the account before handing out links. And rate limits per account and address slow down bulk downloading.