Instagram
0-day streakReview

Your progress

Level 1

0 XP100 XP to level 2
0day streak
0best streak
0topics started
0cards reviewed
    Review cards

    Progress is stored only in this browser. No account is required.

    Layer 4 · Full breakdown · 33 min · 13 figures

    Design Instagram

    Design a photo and short-video sharing app for billions of users: direct-to-storage uploads, an async resize pipeline, needle-style photo storage with hot and warm tiers, sharded IDs, a ranked home feed, 24-hour Stories and like counts on viral posts.

    Instagram looks simple: take a photo, add a caption, share it with your followers. Underneath, it is a media company with a social graph attached. Meta reported in October 2025 that Instagram had passed 3 billion monthly actives, and each of them scrolls images that must appear instantly on a cheap Android phone in Lucknow as well as a new iPhone in London. Three things make the design interesting. The media path: getting a photo into durable storage, turning it into a few sizes and formats, and serving trillions of image views a year without the bill exploding. The social path: building each person’s feed and Stories tray when some accounts have hundreds of millions of followers. And the hot spots: a film star posts wedding photos and ten million likes land on one post within an hour.

    Understand the problem

    Say what kind of system this is. Instagram is read-heavy and media-heavy: each photo is written once, never edited, and viewed hundreds or thousands of times. That tells you to optimise reads, cache aggressively and keep image bytes away from your application servers.

    Functional requirements

    1. Users should be able to upload a photo or short video with a caption, and see it on their profile straight away.
    2. Users should be able to follow other accounts and see a home feed of recent posts from the accounts they follow, newest-and-most-relevant first.
    3. Users should be able to like and comment on posts, and see like and comment counts.
    4. Users should be able to post Stories that disappear after 24 hours and watch the Stories of accounts they follow, with already-watched Stories marked as seen.

    Explore — posts from accounts you don’t follow — is worth a sketch at the end of the deep dives, but it is not a core requirement here.

    Non-functional requirements

    • Scale. 3 billion monthly users; assume 1.5 billion daily active users, 100 million new feed posts and 300 million Story items a day.
    • Fast feed. First page in under 200 ms at p50 and 500 ms at p99; the first image on screen within about a second on 4G. A CDN edge hit for an image takes under 50 ms.
    • Quick publishing. The uploader sees their post immediately; followers can see it within about 5 seconds at p99 of the upload finishing.
    • Availability over consistency. 99.99% for reading feeds and images. Feeds, counts and trays may be a few seconds stale, but you need read-your-own-writes for your own posts, comments and follows, or the app looks broken.
    • Durability. An acknowledged upload survives losing a data centre: three copies before anyone sees the post, a copy in a second region within minutes.
    • Cost. Storage grows by petabytes a month and most of it is rarely read again, so cost per stored photo is a requirement, not an afterthought.

    In the interview

    Ask before you draw: “Photos only, or video too — and how long?” “Is the feed chronological or ranked?” “Are Stories and Explore in scope?” “How many users, and are there accounts with hundreds of millions of followers?” The last answer decides whether pure fan-out on write survives, and the first decides whether you are also designing a transcoding pipeline.

    Out of scope

    • Direct messages. A different problem: delivery, ordering and presence. See design WhatsApp.
    • Reels and long-form video encoding. Short clips go through the same shape of pipeline as photos, but adaptive-bitrate ladders are covered in design YouTube, and a recommendation-first short-video feed in design TikTok.
    • Search, ads, Live and shopping. Each is its own system.
    • Notifications. We emit events; delivering push notifications is its own design.
    • The ranking models themselves. We design where ranking plugs in and its latency budget, not the neural networks.

    The set-up

    Plan the approach

    Split the system in two from the first minute. The media plane moves bytes: uploads, processing, blob storage, CDN. The social plane moves small records: posts, follows, likes, comments, feeds and trays. One scales with bandwidth and disk, the other with queries per second and fan-out, and keeping them apart is the most useful idea in this design. Build each requirement simply, then spend the deep dives on uploads, storage cost, IDs, the feed, Stories and hot counters.

    Core entities

    • User: ID, username, profile, follower and following counts.
    • Post: ID, author ID, caption, list of media IDs, created time, status (processing, ready, deleted).
    • Media: ID, type (photo or video), owner, dimensions, a small blur placeholder, and the list of stored variants (size × format).
    • Follow: an edge from follower to followee.
    • Like and Comment: rows that belong to a post.
    • Story: ID, author, media ID, created and expiry time (created + 24 hours).
    • Seen state: for a viewer and an author, the ID of the last Story the viewer watched.
    • Feed entry: a post ID in a viewer’s precomputed feed list.

    A post lives on its author’s shard, next to its likes and comments:

    -- One logical shard = one Postgres schema, e.g. shard_2993
    CREATE TABLE shard_2993.posts (
      id            bigint PRIMARY KEY DEFAULT shard_2993.next_id(),
      author_id     bigint NOT NULL,
      caption       text,
      media_ids     bigint[] NOT NULL,
      status        smallint NOT NULL,          -- 0 processing, 1 ready, 2 deleted
      like_count    bigint NOT NULL DEFAULT 0,  -- denormalised
      comment_count bigint NOT NULL DEFAULT 0,
      created_at    timestamptz NOT NULL DEFAULT now()
    );
    CREATE TABLE shard_2993.likes (
      post_id bigint, user_id bigint, created_at timestamptz,
      PRIMARY KEY (post_id, user_id)            -- a double-tap twice is still one like
    );

    API

    Create an upload. The phone asks for somewhere to put the bytes before it sends them.

    POST /v1/uploads
    Content-Type: application/json
    
    { "media_type": "photo", "bytes": 1048576, "sha256": "9f2c…", "client_upload_id": "c7b1-…" }
    {
      "upload_id": "up_7Kq2",
      "media_id": 1588708402793464888,
      "upload_url": "https://uploads.example-cdn.net/up_7Kq2?sig=…&expires=900",
      "chunk_bytes": 262144
    }

    The phone then sends the bytes to upload_url, in 256 KB chunks it can resume, never through the API servers.

    Publish a post. Once the bytes are in, publishing is a small JSON call. The Idempotency-Key makes a retried tap safe.

    POST /v1/posts
    Idempotency-Key: c7b1-…
    
    { "media_ids": [1588708402793464888], "caption": "New Year’s Eve on Marine Drive 🎆" }
    { "post_id": 1588708415376376889, "status": "processing" }

    Read the feed with a cursor, never an offset (new posts arrive at the top while you scroll):

    GET /v1/feed?cursor=eyJyIjoxMH0&limit=10
    {
      "posts": [{
        "id": 1588708415376376889,
        "author": { "id": 7654321, "username": "asha.k" },
        "caption": "New Year’s Eve on Marine Drive 🎆",
        "media": [{
          "blur": "LEHV6nWB2yk8",
          "variants": {
            "1080": "https://img.example-cdn.net/m/1588…/1080.avif",
            "640":  "https://img.example-cdn.net/m/1588…/640.avif"
          }
        }],
        "like_count": 1204, "viewer_has_liked": false, "comment_count": 36
      }],
      "next_cursor": "eyJyIjoyMH0"
    }

    Engage and follow:

    POST   /v1/posts/{id}/likes             like (idempotent)
    DELETE /v1/posts/{id}/likes             unlike
    POST   /v1/posts/{id}/comments          body: text
    GET    /v1/posts/{id}/comments?cursor=  newest or top comments, paged
    POST   /v1/users/{id}/follow            follow; DELETE to unfollow

    Stories:

    POST /v1/stories                        body: media_id; expires 24 h after creation
    GET  /v1/stories/tray                   authors with active Stories, unseen first
    GET  /v1/stories?author_id=…            that author’s active items
    POST /v1/stories/seen                   body: [{ author_id, last_seen_story_id }]

    Back-of-envelope estimates

    These numbers decide where the money and the engineering go. (For the method, see back-of-envelope estimation.)

    Uploads. 100M feed posts a day, assume 90% photos and 10% short videos. 300M Story items, assume two-thirds photos.

    • Feed posts: 100M ÷ 86,400 ≈ 1,200 posts/s. Stories: 300M ÷ 86,400 ≈ 3,500 items/s.
    • Together about 4,600 uploads a second on average; at a 3× evening peak (and far more at midnight on New Year’s Eve), plan for ≈ 14k uploads/s.
    • Photos per day: 90M + 200M ≈ 300M photos/day to process.

    Processing. Assume one photo costs 0.6 CPU-seconds to decode, resize and encode into four sizes and two formats. 300M × 0.6 s = 180M CPU-seconds a day ÷ 86,400 ≈ 2,100 cores on average, ≈ 6,300 at peak — a modest fleet behind a queue.

    Storage. A downscaled 1 MB master plus about 0.5 MB of variants is 1.5 MB per photo; a short video with renditions is about 25 MB.

    • Feed photos: 90M × 1.5 MB = 135 TB/day.
    • Feed videos: 10M × 25 MB = 250 TB/day — already more than all the photos.
    • Total: ≈ 385 TB/day × 365 ≈ 140 PB of new media a year.
    • Stories: 300M × ~2 MB ≈ 600 TB/day, hot for only 24 hours: about 600 TB rolling, not a growing pile.
    • Three copies on RAID (3 × 1.2 = 3.6 bytes of raw disk per byte): 140 × 3.6 ≈ 504 PB of raw disk per year of uploads.

    Metadata. Posts at about 1 KB each: 100M × 1 KB = 100 GB/day, about 37 TB a year. Likes: 1.5B users × 3 = 4.5B likes/day ≈ 52k/s, about 150k/s at peak; at about 100 bytes each with index, 450 GB/day ≈ 165 TB a year — four times the posts.

    Reads.

    • Feed: 1.5B × 10 page loads a day = 15B ÷ 86,400 ≈ 175k feed requests/s, about 350k/s at peak.
    • Images: assume each daily user sees 200 images (feed, Stories, profile grids): 1.5B × 200 = 300B a day ÷ 86,400 ≈ 3.5M image requests/s, about 7M/s at peak.
    • Bandwidth: 3.5M/s × 60 KB average = 210 GB/s × 8 ≈ 1.7 Tbps of image egress, 3.4 Tbps at peak, before video.
    • Origin: if the CDN serves 90% (Facebook’s Haystack paper reported its storage answering about 10% of photo requests that reached its CDNs), 350k/s fall through; if an origin cache of recent photos takes half, ≈ 175k reads/s hit disk.

    Three numbers shape the design: 3.5M image requests a second (the CDN and image sizes matter more than any database), 175k cold reads a second (each must cost one disk read, not three), and 504 PB of raw disk a year (old photos must move to cheaper storage).

    Tip

    When you size Instagram, don’t stop at QPS. Write down bytes per second leaving the CDN and raw petabytes per year. In a media-heavy design, those two numbers are the cost of the company.

    High-level design

    Build one requirement at a time. By the end of this section the design works for a few million users in one region; the deep dives make it survive three billion.

    Users should be able to upload a photo or short video with a caption

    The naive version posts a multipart form to an API server. It fails in production: a photo sent over patchy 3G from a moving train ties up an API worker for 20 seconds, and at 14k uploads a second the API tier moves 14 GB/s of bytes it never needed to touch. Separate the bytes from the metadata instead:

    Step 1: photo bytes go straight from the phone to object storage; the post service only handles small JSON calls.Notice Resizing and re-encoding happen after the upload is safely stored, in workers fed by a queue, so a slow encoder never keeps the user waiting at the Share button.
    1. The phone downscales the photo (a 4–6 MB camera file becomes about 1 MB) and calls POST /v1/uploads, which returns a media ID and a short-lived signed URL.
    2. The phone sends the bytes directly to object storage.
    3. POST /v1/posts writes the post with status processing and queues a job.
    4. Workers make the variants, write them to the media store and flip the post to ready. Only then can followers see it.

    Early Instagram did essentially this: its 2011 stack write-up has photos going “straight to Amazon S3” behind a CDN, with slow work on a task queue so “media uploads can finish quickly”.

    Users should be able to follow accounts and see a home feed

    A follow is an edge in a follow graph, stored twice: by follower (to build my feed) and by followee (to fan out my posts).

    The simplest feed is fan-out on read: fetch recent posts from all 300 accounts you follow, merge and sort. At 175k feed loads a second that is about 50M shard queries a second. So precompute: fan-out on write. When a post becomes ready, a worker pushes its ID onto each follower’s feed list, and reading a feed becomes one lookup.

    Step 2: a feed read takes post IDs from the viewer’s precomputed feed, fills in post details from a cache, and leaves the pixels to the CDN.Notice The feed service never touches image bytes: it returns URLs, and the phone fetches them from the nearest CDN edge.

    The feed service hydrates the IDs (caption, author, counts, whether you liked it) from a post cache and returns image URLs; the phone fetches the images from the CDN. Instagram’s 2011 write-up describes this shape — the main feed in Redis, fan-out done “in Gearman”, a task queue — and Feed later moved to Cassandra. Celebrities and ranking are deep dives; the full treatment of fan-out is in design a news feed.

    Users should be able to like and comment on posts

    A like is a row keyed by (post_id, user_id), so liking twice is still one like. Comments are rows keyed by post and comment ID. Both live on the post’s shard, so the like row and the post’s like_count can change in one local transaction.

    Step 3: a like inserts one row and bumps the post’s counter, in the same transaction, on the shard that owns the post.Notice The like and its count can never disagree, because they commit together; notifications hear about it later through an event.

    Instagram made this move when it went multi-region: instead of a memcache counter with SELECT COUNT(*) on a miss, it denormalised the like count into the database in the same transaction as the like, so reading it became a simple select. A post getting 15,000 likes a second is a different animal, and gets its own deep dive.

    Users should be able to post Stories that disappear after 24 hours

    A Story item is an ordinary media upload with an expiry 24 hours after creation. What is new is the read side: the tray shows every account you follow with a live Story, with a coloured ring if there is something you haven’t watched.

    Step 4: Stories add three small stores — who has live Stories, the items themselves, and what each viewer has already seen.Notice Everything here carries a 24-hour lifetime; nothing in the Story path needs a delete job.
    • Posting writes the item with a 24-hour TTL and adds its ID to the author’s entry in the active-stories index.
    • The tray checks each account you follow for live Stories and puts unseen ones first.
    • Watching reports “seen up to Story X of author Y”, updating seen state so the ring turns grey on all your devices.
    • Expiry is just the TTL; the owner keeps a private copy in an archive.

    You now have a working Instagram for a few million people. Everything in it bends at three billion.

    Deep dives

    How do we upload and process photos without making anyone wait?

    The user’s experience should be “I tapped Share and it was there” — on a ₹8,000 Android phone with a flaky connection, on Diwali night when everyone posts at once.

    Bad: upload through the API servers. One request does everything, and you pay three times. The API tier carries 14 GB/s at peak, so you buy servers for bandwidth rather than logic. Slow clients hold workers for the whole upload, so a few thousand weak 3G connections can exhaust a pool. And a drop at 90% restarts from zero.

    Good: one signed PUT straight to object storage. The API hands out a URL valid for one object, one size limit and about 15 minutes, and the phone sends the bytes to object storage directly. The API tier sees only small JSON calls. You pay with a two-step protocol — publish must check that the object exists and matches the declared size and hash — and a dropped connection still restarts the whole file.

    Great: resumable chunks, started early, processed asynchronously.

    • Shrink before you send. The app never displays a 12-megapixel original, so the phone downscales to the largest size it will show (say 1,440 px on the long edge): 4–6 MB becomes about 1 MB, which is 1 second on an 8 Mbps 4G link and 4 seconds on 2 Mbps. It is the biggest latency win, and it cuts storage too.
    • Chunk and resume. Send 256 KB chunks against an upload ID; after a drop, ask “what offset do you have?” and continue — the standard large file upload pattern.
    • Start before Share. Begin uploading as soon as the user picks a photo, while they choose a filter and type the caption, so tapping Share is one small call. Upload the unfiltered photo and send crop and filter as parameters for the workers to apply. You pay for abandoned uploads; a lifecycle rule deletes unpublished originals after 24 hours.
    • Make publish idempotent. A key generated on tapping Share means a retried request returns the same post instead of a second one (see idempotency).

    Then the bytes go through a pipeline the user never waits on.

    What happens to one photo after it lands in storage. Times are per photo on one core and are assumptions, not measurements.Notice Where the privacy step sits: before any variant exists, so no copy that leaves the building carries the photo’s GPS location.

    Each stage earns its place:

    • Validate by content, not by name. Check the magic bytes, not the extension, and refuse anything that decodes to more than, say, 50 megapixels before allocating memory. A tiny file that expands into a gigantic bitmap — a decompression bomb — is a classic way to knock over image workers.
    • Sanitise for privacy. EXIF metadata often holds the exact GPS coordinates of someone’s home. Apply the EXIF orientation to the pixels, keep the colour profile, strip everything else — before any variant is written.
    • Resize to a fixed ladder. 1,080 px for full screen, 640 for the feed, 320 for profile grids, 150 for thumbnails. Every phone asks for one of four URLs, not one of a thousand, which keeps CDN hit ratios high.
    • Encode twice. A modern format for clients that accept it, JPEG for the rest. Google measures lossy WebP at 25–34% smaller than JPEG at equal quality, and AVIF typically does better; at 3.5M image requests a second, a quarter fewer bytes is a quarter off the CDN bill. You pay CPU: AVIF is the most expensive stage.
    • Publish last. Only when every variant is durable does the post flip to ready and emit the event that starts fan-out.

    What the user sees meanwhile. The phone already has the pixels, so it shows the post on the user’s own profile at once with a “Posting…” indicator. Followers see nothing until ready: a post with missing images is worse than one that arrives two seconds later. Processing also computes a blur hash of 20–30 characters that rides in the feed JSON, so the feed paints a soft blur of the right colours before the image arrives.

    When things fail. Workers are idempotent per media ID, so a retried job rewrites identical objects. A sweeper re-enqueues anything stuck in processing for two minutes; repeated failures go to a dead-letter queue and the user sees “Couldn’t post — tap to retry”. Short videos take the same path with a transcode stage; bitrate ladders are covered in design YouTube.

    Never trust the client’s bytes

    The phone resizes and compresses to save bandwidth, but the server still decodes, validates, re-encodes and strips metadata itself. A modified client can upload anything; only the server-made variants are ever served to other people.

    How do we store billions of photos and serve them cheaply?

    Start with the CDN, because it does most of the work. An image URL names the media ID, size and format, and its bytes never change — an edited photo gets a new media ID — so every variant can be cached with Cache-Control: public, max-age=31536000, immutable, and the edges answer about 90% of requests (more in CDN).

    The trouble is the other 10%: the long tail. A friend scrolls back to your 2019 trip to Leh; nobody has looked at those photos for months, so no edge has them. That is our 175k cold reads a second, spread across hundreds of petabytes. What does each one cost?

    Bad: one file per photo on a regular file system. The obvious design, and the one Facebook ran first on network-attached storage. One photo costs about three disk reads: the directory (name to inode number), the inode, then the data. The metadata is only free if it stays in memory, and at billions of files it doesn’t — an XFS inode structure alone is 536 bytes.

    Good: a managed object store. Right for almost every company: durability, replication and lifecycle rules out of the box, and early Instagram kept its photos in S3 behind CloudFront. You pay per request and per gigabyte, and at three billion users those bills justify building your own.

    Great (at Meta scale): many photos per file, with an in-memory index. This is Haystack’s core idea. Append photos — each one a needle — to large volume files of about 100 GB, and keep a tiny in-memory map from photo key to offset and size. A cold read is one seek and one read.

    The same cold photo read, with one file per photo (left) and many photos per volume file (right).Notice The photo bytes are the same on both sides; what changes is where the metadata lives — on disk on the left, in RAM on the right.

    The details are what make it work:

    • Tiny metadata. Haystack used about 10 bytes of RAM per photo. A machine holding 10 TB of 100 KB photos has 100M needles: 100M × 10 B = 1 GB of RAM for the index. As files, the inodes alone would be 100M × 536 B ≈ 54 GB.
    • Append-only. A delete sets a flag; compaction later copies live needles into a new file. Deletes matter: Facebook saw about 25% of photos deleted within a year, mostly young ones.
    • A cookie in the URL. Each needle stores a random number that the request must match, so nobody can scrape private photos by guessing URLs.
    • Replicas and an index file. Each logical volume is written to three physical volumes on different machines, and a checkpoint of the in-memory map lets a restarted machine recover without rereading terabytes.
    • Shelter fresh photos. New photos are read most and sit on machines still taking writes; Haystack’s internal cache in front of exactly those machines measured an 80% hit rate.

    The cost: you now own a storage system — compaction, rebalancing, disk failures and a directory of volumes. Below hundreds of petabytes, a managed object store wins.

    Hot and warm tiers. Storing every photo three times on RAID is still expensive, and popularity decays fast: in Facebook’s f4 paper, week-old content was requested an order of magnitude less than content under a day old. So photos older than three months (f4’s threshold for photos) move to a warm store that uses erasure coding instead of full copies.

    Left: where a photo request is answered as the photo ages. Right: raw disk per byte stored in each tier, from Facebook’s published f4 figures.Notice The warm tier does not change how photos are read through the CDN; it changes how many bytes of disk each stored byte costs.

    The arithmetic from f4: three replicas on RAID-6 (1.2×) is 3 × 1.2 = 3.6 bytes of disk per byte. Reed-Solomon (10,4) stores 4 parity blocks per 10 data blocks, a 1.4× overhead that survives losing any 4; a coded copy in two regions gives 2.8, and replacing the second copy with an XOR of volumes from two regions gives 2.1. Almost everything stored is older than three months, so our 140 PB a year needs about 294 PB of raw disk instead of 504 PB — 210 PB less for every year of uploads.

    You pay three ways: rebuilding a lost block means reading 10 others, so the warm tier can only serve warm traffic; migration moves petabytes in the background; and a router must know which tier holds each photo. That is a fair price for 42% of the disk budget.

    In the interview

    The sentence to say: “The CDN absorbs most reads, so the storage problem is the long tail. I keep per-photo metadata in RAM so a cold read is one disk seek, and I move photos older than about three months to erasure-coded storage, which cuts raw disk per byte from 3.6 to about 2.1.”

    How do we generate IDs and shard the metadata?

    Once posts live on many database primaries, auto-increment keys collide. You need IDs that are unique across shards, sortable by time (feeds and pagination sort by ID), 64 bits (smaller indexes) and — Instagram’s own 2012 requirement — made with as few new moving parts as possible. (The general problem is design a unique ID generator.)

    Bad: one “ticket” database hands out every ID. Simple and sortable, but a single point of failure and a write bottleneck; two ticket servers on odd and even numbers fix availability but lose time order.

    Good: random UUIDs, or a Snowflake-style service. UUIDs need no coordination but are 128 bits, unordered, and scatter inserts across the B-tree. A Snowflake-style service packs time, worker number and sequence into 64 sortable bits; you pay with a new tier of servers and a coordinator such as ZooKeeper.

    Great: generate the ID inside the database that stores the row. Instagram packs three fields into 64 bits with a small PL/pgSQL function used as the column default on every sharded table:

    Top: the 64-bit post ID and a worked example. Bottom: the shard in the ID picks a logical shard, and a small map picks the server.Notice Any service can route a post ID to its database with a bit shift and one map lookup — no directory service on the hot path.
    • 41 bits of milliseconds since a custom epoch: 2⁴¹ ms ≈ 2.2 × 10¹² ms ≈ 69 years. Time in the top bits means sorting by ID is sorting by creation time.
    • 13 bits of logical shard: up to 8,192 shards, so any service finds a post’s database from the ID alone.
    • 10 bits of sequence from a per-table Postgres sequence modulo 1,024: 1,024 IDs per shard per millisecond, far above our 14k uploads a second in total.

    In the example, user 7,654,321 lives on shard 7,654,321 mod 8,192 = 2,993; their 12,345th row there gets sequence 12,345 mod 1,024 = 57; and midnight on 1 January 2026, with a 1 January 2020 epoch, is 2,192 days × 86,400,000 ms = 189,388,800,000.

    You pay twice. Time comes from the database host’s clock, so a clock that jumps backwards can produce out-of-order IDs — run NTP in slew mode and alarm on drift. And 13 bits caps you at 8,192 logical shards, so choose generously on day one.

    DB auto-increment Random UUID (v4) Snowflake-style service ID inside each shard
    Size 64 bits 128 bits 64 bits 64 bits
    Sorted by time Only within one database No Yes, to the millisecond Yes, to the millisecond
    New moving parts A ticket database None ID servers + coordinator None — a SQL function
    Index locality Good Poor: random inserts Good Good
    Finds its shard No No No (worker ID, not shard) Yes, from the bits
    Main risk Single point of failure Bigger indexes, no order Another service to run Clock skew on DB hosts
    Ways to generate IDs for a sharded photo app

    Logical shards, not physical ones. Instagram created several thousand logical shards — each a Postgres schema holding every sharded table — mapped in code to far fewer servers; in 2011 the main cluster was 12 database instances, each with a replica in another zone. When a server fills up, move a range of logical shards: replicate those schemas, pause their writes briefly, flip the map, resume. Nothing is rehashed, because user-to-logical-shard never changes (see sharding a relational database).

    Shard by author. A user’s posts and each post’s likes and comments share the author’s shard, so a profile page is one query and like-plus-counter is one local transaction. You pay for celebrities: a star’s shard takes far more traffic than its neighbours, so move the logical shard holding a very hot account onto its own server.

    Across regions. Instagram’s 2015 move to several data centres kept one Postgres primary per shard with read replicas in every region: reads are local, writes cross regions. To keep regional caches honest, a write inserts a cache-invalidation row in the same transaction, replication carries it, and a process in each region invalidates its cache. Regions converge within one replication delay (see multi-region and active-active).

    How do we build the home feed — and what is different about a photo feed?

    The fan-out machinery is the same as in any social feed, and design a news feed covers it end to end. Three things differ enough on Instagram to be worth your interview time.

    Celebrities break pure fan-out on write. 100M posts a day × 200 followers ≈ 20B feed inserts a day, about 230k a second — big but steady. A film star with 300 million followers posting during the IPL final means 300 million inserts for one post: five minutes at a million inserts a second, with the queue jammed for everyone else. So use a hybrid: fan out on write for normal accounts, and for accounts above about 1 million followers, pull their recent posts at read time. A user follows a few dozen such accounts at most, so the pull is a few cached lookups (see push vs pull).

    The feed is ranked. Instagram’s public explanation of Feed ranking describes candidates from followed accounts plus suggestions, thousands of signals, and about a dozen predictions per post — weighted most heavily: spending a few seconds on it, commenting, liking, sharing, and tapping the author’s profile — plus rules against too many posts from one author in a row. For the designer, ranking is a stage with a latency budget.

    A feed read: three candidate sources are merged, ranked, and returned as one page — and the phone starts downloading the next page’s images before you get there.Notice The dashed prefetch arrow: the images for posts 11–20 are already on the phone by the time the user scrolls to them.

    A workable budget: fetch the feed list and celebrity posts in parallel (about 10 ms), merge and filter about 500 candidates (20 ms), fetch features and score (60 ms), hydrate the top 10 (20 ms) — roughly 110 ms at p50, inside the 200 ms target. Rank the next page in the same request and cache it.

    Images decide how fast the feed feels. A 110 ms API still feels slow if photos trickle in. Four techniques matter more than any server tuning:

    • The right size. A small-screen phone fetches the 640 px AVIF (about 40–60 KB), not the 1,080 px JPEG at three to four times the bytes; on 2G, step down a size.
    • Blur first. The blur hash in the JSON paints instantly; the image fades in.
    • Prefetch ahead of the thumb. When page one arrives, download images for posts 2–5; at post 5, fetch page two and its first images. Ten images is about 0.5 MB — trivial on Wi-Fi, so limit depth on metered data or battery saver.
    • Warm the edge for big accounts. Millions of phones will ask for a celebrity’s four variants within minutes; push them to regional CDN caches at publish time so the first wave doesn’t all miss.

    The feed store. 500 post IDs × 8 bytes = 4 KB a user, about 8 TB for 2 billion lists. Instagram moved Feed from Redis to Cassandra, and its Rocksandra write-up describes swapping in a RocksDB storage engine and cutting one cluster’s p99 read latency from 60 ms to 20 ms — the feed store’s tail latency is the feed’s tail latency.

    How do Stories expire, and how do we know what each viewer has seen?

    Stories differ from posts in three ways: everything dies after 24 hours, the tray shows per viewer who has something new, and people tap through fast, so the next one must already be downloaded.

    Expiry. Bad: a cron job deleting expired rows every minute. It scans huge tables, deletes in bursts, and when it falls behind, expired Stories stay visible. Good: filter at read time on expires_at — correct to the second, whatever the cleanup does. Great: filter at read time and let TTLs clean up. Rows and index entries carry a 24-hour TTL, media objects a lifecycle rule; in a Cassandra-style store, group data by time window so whole files expire together rather than leaving a tombstone per row. The owner’s copy moves to a private archive in the cheap tier.

    Building the tray. You follow about 300 accounts; perhaps 60 have a live Story. Push — adding the author to each follower’s tray on post — is the feed’s fan-out problem again, celebrities included. Pull — looking up all 300 followees in the active-stories index in one pipelined batch — takes a few milliseconds from memory, and the index is small and expires on its own. Pull wins; cache each viewer’s tray for a minute or two.

    Ordering the tray. Instagram says Stories ranking uses viewing history, engagement history and closeness to predict whether you will tap in, reply or skip. Unseen authors go first by that score, then seen ones.

    Seen state. Bad: check per-Story view rows. Colouring one tray means looking up every live Story of 60 authors — hundreds of reads per app open, against a table growing by 30B rows a day. Great: one pointer per (viewer, author). Story IDs are time-sortable, so store only the last Story ID the viewer watched from that author. The ring is unseen exactly when the author’s newest live Story has a larger ID.

    How the tray decides ring colours: one active-stories list per author and one last-seen pointer per viewer–author pair.Notice Meera has no seen row at all, and that is enough to know everything she posted is new to Asha.

    Size it: 1.5B daily users × about 60 authors ≈ 90B pairs × 24 bytes ≈ 2.2 TB with a TTL, sharded by viewer. Updates keep the maximum, so duplicate or out-of-order “seen” events from two devices are harmless.

    The author still wants the viewer list (“seen by 214”), which really is per (Story, viewer): 300M Stories × about 100 views = 30B events a day ÷ 86,400 ≈ 350k events a second. The phone batches them every few seconds into a log such as Kafka, and consumers append to viewer lists and bump view counts; five seconds of lag is fine.

    Instant playback. When you open someone’s Stories, the app downloads the current item and the next; while you watch, it fetches the first item of the next author in the tray. Story media is hot for only 24 hours, so cache hit rates are very high, and big accounts’ Stories can be pushed to the edge at publish.

    How do we count likes on a viral post?

    A Bollywood star posts wedding photos. Ten million likes arrive in the first hour — 10M ÷ 3,600 ≈ 2,800 a second on average, perhaps 15,000 a second in the first minutes — all on one post, one shard, one row.

    Bad: count on read. SELECT COUNT(*) on every feed view; Instagram’s 2015 write-up says such a query “would take hundreds of milliseconds”.

    Good: a denormalised counter in the same transaction. Insert the like and UPDATE posts SET like_count = like_count + 1 together, as Instagram did. Reads are a key lookup and the count never drifts. Right for 99.9% of posts, but not the star’s: every update takes the same row lock, so 15,000 transactions a second queue up, and each like invalidates the cached count just as thousands of servers want to read it.

    Left: every like updates the same row. Right: like rows are still written, but the counter is updated once a second with the sum.Notice The right side keeps exact like rows (who liked what) and only makes the displayed number approximate for about a second.

    Great: keep the rows exact, batch the counter, and stop caring about the last digit.

    • Rows stay exact. Each like is still a row keyed (post_id, user_id), so likes are idempotent and “did I like this?” is a key lookup. Inserts spread across index pages instead of fighting over one row lock.
    • The counter is batched. Each like emits an event; an aggregator sums them per post and once a second runs one UPDATE … SET like_count = like_count + 15000. One counter write a second instead of fifteen thousand (see scaling writes).
    • Or split the counter. Without a stream, use 16 counter rows (post_id, k): each like updates a random k, reads sum all 16 and cache the sum. Contention drops 16×. Enable it only for posts above a likes-per-second threshold.
    • Protect the cache. Instagram used memcache leases: the first server to miss gets a token to refill the key; the rest get the stale value or wait briefly (see hot keys and cache stampede).
    • Display approximately. Nobody can tell 1,204,311 from 1,204,377; show “1.2M likes”. The viewer’s own heart turns red on tap, from the client.

    You pay with a count up to a second stale and an aggregator to run; you gain a shard that survives the biggest post of the year.

    How does Explore find posts from accounts you don’t follow?

    Explore’s candidates come from everyone you don’t follow, out of billions of posts. You won’t design the models in this interview, but know the shape: the retrieve-then-rank funnel every large recommender uses, as Meta described for Explore in 2019.

    Explore as a funnel: cheap steps over many candidates, expensive models over few.Notice The most expensive model only ever scores 50 posts, out of tens of thousands that were eligible.
    • Retrieve by account. Start from seed accounts you have liked or saved from, and find similar accounts with ig2vec — embeddings trained word2vec-style on the sequences of accounts people interact with in a session — using a nearest-neighbour index (Meta’s FAISS). Their recent posts become candidates. Accounts capture niche communities — vintage tractors, Carnatic music — that no hand-made topic list would (see embeddings and semantic search).
    • Filter first. Remove content that breaks the rules or that the recommendation guidelines say should not be shown to strangers; the bar for recommending is higher than for following.
    • Rank in stages. 500 sampled candidates go through a distilled model (keeps 150), a lightweight network (50) and a deep multi-task network (25 for the first page), scored as w_like × P(like) + w_save × P(save) − w_neg × P(see fewer) with a penalty for repeating an author. Meta reported 65 billion features extracted and 90 million predictions made per second.

    The surrounding system is in design a recommendation feed; a product whose whole feed is such a funnel is design TikTok.

    The final design

    The two planes meet only through IDs and URLs: small records at high QPS above, bytes at high bandwidth below.

    The complete design: a social plane for posts, feeds, likes and Stories, and a media plane for bytes.Notice The phone talks to the media plane directly in both directions — uploads to object storage, downloads from the CDN — so no application server ever carries a photo.

    Follow a photo from the moment it is taken to the moment ten million people like it:

    1. Upload. The phone downscales the photo and sends resumable chunks to object storage with a signed URL, usually while the caption is still being typed.
    2. Process. Publishing queues a job; workers validate, strip EXIF, make four sizes in AVIF and JPEG, write them to the hot store’s needle volumes and mark the post ready.
    3. Publish. The post row, with a 64-bit ID encoding time and shard, commits on the author’s logical shard; the same transaction records an event.
    4. Fan out. Workers push the post ID onto each follower’s feed list — unless the author has over about a million followers, whose posts readers pull.
    5. Read the feed. The feed service merges pushed IDs, celebrity posts and suggestions, ranks about 500 candidates, hydrates the top 10 and returns image URLs with blur placeholders.
    6. Load images. The phone fetches the right variant from the CDN and prefetches the next page. About 90% of requests stop at the edge; misses go to the origin cache, then the hot store, or the warm store for photos older than about three months.
    7. Like. A like inserts a row on the post’s shard and emits an event; the like counter adds a batched delta once a second.
    8. Stories. Live Stories sit in a 24-hour TTL store; trays are pulled from the active-stories index; seen state is one pointer per viewer and author; items move to the owner’s archive.

    Against the requirements: about 110 ms of server work per feed page at p50; 90% of 3.5M image requests a second answered at the edge and one disk seek per cold read; followers see posts seconds after upload, celebrities’ instantly by pull; three copies before anyone sees a post; reads served from regional replicas and caches, so a slow primary delays writes, not feeds; and raw disk per byte cut from 3.6 to 2.1 for old photos.

    What interviewers look for by level

    Area Mid-level Senior Staff
    Requirements Upload, feed, likes Read-heavy, media-heavy; sets latency, durability and staleness budgets Frames media cost (egress, petabytes a year) as a requirement; names which features may be stale
    Upload path Upload to a server, store in S3 Signed URLs, async pipeline, “processing” state Resumable and speculative uploads, EXIF stripping, decompression bombs, idempotent publish
    Media storage “Use S3 and a CDN” Variants by screen size, immutable URLs, CDN hit ratio Long-tail cost per read, needle volumes, hot versus warm tiers with replication arithmetic
    Data model and IDs One posts table Shards by user, time-sortable IDs Logical shards, ID bits that route to a shard, clock-skew risk, moving shards without rehashing
    Feed Fan-out on write Hybrid fan-out for celebrities, cursor pagination Ranking budget, prefetching and edge pre-warming, the feed store’s tail latency
    Stories Delete after 24 hours TTLs, a tray built at read time Seen state as one pointer per author, batched view events, prefetching the next author
    Hot spots Cache the counts Denormalised counter, cache stampede Batched or sharded counters, leases, approximate display, per-post hot-key detection

    Follow-up questions

    What if a user deletes a post, or the law requires a photo to disappear everywhere?

    Mark the post deleted on its shard, invalidate the post cache, and remove the ID from feed lists in the background, so it vanishes from API responses within seconds. The bytes are harder: immutable URLs sit in edge caches for up to a year. Purge the CDN by media ID (most CDNs can purge by a tag on the response), flag the needles so the store refuses them at once, and let compaction reclaim the space. For a legal takedown, record the request and verify every tier — edge, origin cache, hot, warm, archive, backups — has dropped the object. The honest guarantee is “unreachable now, physically deleted after compaction”.

    What if a whole region goes down?

    Each region has replicas of every shard, its own caches and a full media path, so DNS or anycast shifts traffic away and users see slightly stale feeds rather than errors. Writes are harder because each shard has one primary: promote a replica elsewhere, accepting the loss of writes inside the replication lag, or keep those shards read-only until the region returns. Instagram’s 2015 post describes surviving a planned peak-hour shutdown of a whole data centre without users noticing — the kind of result you only get by practising failovers (see disaster recovery).

    What if we want to add a new format, such as HDR photos or a better codec?

    Keep the master from upload and treat every variant as derived data. Add the new format to the pipeline for new uploads, then backfill lazily: when a client asks for a missing variant, serve the old format and queue a job to make the new one, so CPU goes only to photos people actually view. Most warm-tier photos may never be converted. HDR is a good example — a new variant type plus client changes, with no change to posts or feeds.

    What if users in low-bandwidth areas complain that the feed is slow?

    Measure from the phone — time to first image, by network type — not the server. Then cut bytes: smaller variants and stronger AVIF compression on slow networks, shallower prefetch with data saver on, blur placeholders so the layout never jumps. Put CDN capacity close to those users and peer with their mobile networks, because a request that crosses a continent costs 200 ms before the first byte. None of this changes the backend; all of it changes what the user feels.

    What if a celebrity posts during the IPL final, when traffic is already at its peak?

    Everything hot gets hotter at once. The hybrid feed means no fan-out storm; variants pushed to the edge at publish mean the first million image requests don’t all miss; the post’s like counter switches to batched mode as its rate crosses a threshold; and “top comments” are cached and refreshed every few seconds. If the system is still overloaded, shed the least important work first — delay counts, keep feeds and uploads — with backpressure and load shedding.

    How to say it in the interview

    “I’ll design Instagram for about 1.5 billion daily users: 100 million posts and 300 million Stories a day, and roughly 3.5 million image requests a second. I’ll split it into a media plane for bytes and a social plane for metadata.

    For uploads, the phone downscales the photo and sends it in resumable chunks straight to object storage with a signed URL, often before the user taps Share. Publishing is a small idempotent call that writes the post as ‘processing’ and queues a job. Workers validate the image, strip EXIF data like GPS, make four sizes in AVIF and JPEG, and then mark the post ready.

    Serving images is mostly a CDN problem: immutable URLs and a fixed set of sizes give about a 90% hit rate. The long tail is the storage problem, so photos live in large append-only volume files with an in-memory index — one disk read per cold photo — and move to erasure-coded warm storage after about three months, which cuts raw disk per byte from 3.6 to about 2.1.

    Metadata is in Postgres, sharded by author into thousands of logical shards mapped onto fewer servers. IDs are 64-bit: time, shard and a sequence, so they sort by time and route to their shard.

    The feed is fan-out on write into per-user lists, except for accounts with millions of followers, which are pulled at read time. Then about 500 candidates are ranked, and the phone prefetches the next page’s images.

    Stories expire by TTL. The tray is built by pulling from an index of live Stories, and seen state is one pointer per viewer and author, because IDs are time-ordered.

    Likes are rows plus a denormalised count. For viral posts the count is batched once a second and shown as ‘1.2 million’, so one hot post can’t melt a shard.”

    Concepts used in this design

    References and further reading

    1. Sharding & IDs at Instagram (Instagram Engineering, 2012)medium.comThe 41-bit time + 13-bit logical shard + 10-bit sequence ID generated by a PL/pgSQL function, and thousands of logical shards (Postgres schemas) mapped to fewer servers.
    2. What Powers Instagram: Hundreds of Instances, Dozens of Technologies (Instagram Engineering, 2011)medium.comEarly stack: photos in S3 behind a CDN, sharded PostgreSQL with replicas, Redis for the main feed, and feed fan-out run on background task workers.
    3. Instagration Pt. 2: Scaling our infrastructure to multiple data centers (Instagram Engineering, 2015)medium.com400M monthly users and 40B photos and videos in 2015; replicated cache-invalidation events, denormalised like counters in the same transaction, and memcache leases for hot keys.
    4. Open-sourcing a 10x reduction in Apache Cassandra tail latency (Instagram Engineering, 2018)medium.comCassandra backs Feed and Direct; replacing the storage engine with RocksDB cut one cluster’s p99 read latency from 60 ms to 20 ms.
    5. Finding a needle in Haystack: Facebook’s photo storage (Beaver et al., OSDI 2010)usenix.orgWhy one file per photo costs several disk reads, needles in 100 GB volume files, about 10 bytes of RAM per photo, cookies in URLs, and an 80% hit rate for an internal cache of recent photos.
    6. f4: Facebook’s Warm BLOB Storage System (Muralidhar et al., OSDI 2014)usenix.orgRequest rate falls about 10× within a week; photos move to warm storage after three months; effective replication drops from 3.6 to 2.8 and then 2.1 with Reed-Solomon and XOR coding.
    7. Instagram Ranking Explained (Instagram, 2023)about.instagram.comSeparate rankers for Feed, Stories, Explore and Reels; the Feed predictions (dwell, comment, like, share, profile tap) and the Stories signals (viewing history, engagement, closeness).
    8. Powered by AI: Instagram’s Explore recommender system (Meta AI, 2019)ai.meta.comSeed accounts, ig2vec account embeddings with FAISS nearest-neighbour search, and the 500 → 150 → 50 → 25 three-stage ranking funnel.
    9. Meta Q3 2025 earnings: prepared remarkss21.q4cdn.comInstagram reached 3 billion monthly actives (October 2025).
    10. WebP, a modern image format (Google for Developers)developers.google.comLossy WebP files are 25–34% smaller than JPEG at equivalent SSIM quality.