Instagram Photo Sharing System Design

Instagram is a photo- and video-centric social network: users upload photos and short videos, follow other users, view a personalized feed of media from accounts they follow, post ephemeral 24-hour “stories”, direct-message peers, and discover content via search and the algorithmic Explore page. The canonical industrial deployment is Instagram itself (acquired by Facebook in 2012, now part of Meta; reported to have crossed 3 billion monthly active users in September 2025, up from the ~2 billion it reported in late 2022 — see §2.2 for the as-of caveat). The system shares architectural DNA with Twitter Newsfeed System Design — fanout of an authored item to followers’ feeds, with the same celebrity hot-key problem — but the photo and video payloads dominate the design in ways tweets do not. Image and video storage, CDN-fronted media delivery, upload pipeline orchestration (resize, transcode, thumbnail, possibly content moderation via ML), and the canonical Facebook Haystack photo-store paper (Beaver et al. OSDI 2010) are the load-bearing pieces. The interview question tests whether the candidate can layer the social-feed mechanics from Twitter onto a media-heavy storage stack.

1. Why This System Appears in Interviews

Interviewers like the Instagram problem because:

  • It builds on the Twitter timeline pattern and probes whether the candidate can extend rather than re-derive the feed mechanics.
  • Photo storage is itself a textbook hard problem (small-file storage at scale — Haystack’s 2010 paper is a primary reference).
  • Image processing pipelines exercise async-task orchestration, idempotency, and the producer/consumer pattern.
  • The asymmetry between feed reads (frequent) and uploads (rare per user, but each upload is heavy I/O) forces clean separation of write and read paths.
  • Stories add a TTL dimension absent from Twitter, exercising garbage collection and ephemeral content.
  • The follow graph is undirected from a privacy-policy standpoint (private accounts) but directed from a feed-construction standpoint, complicating both backfill and fanout.

2. Requirements

2.1 Functional Requirements

  1. Upload photo or video. A user uploads media (1–100 MB photo, up to a few hundred MB video) plus caption, location tag, hashtags, mentions.
  2. Feed. Read a personalized feed of recent posts from followed accounts, in chronological-or-ranked order.
  3. User profile. Visit any user’s profile to see their posts in reverse chronological order.
  4. Follow / unfollow. Public accounts can be followed by anyone; private accounts require approval.
  5. Like / comment. Engagement actions on posts.
  6. Stories. 24-hour ephemeral posts that appear in a separate stories tray rather than the main feed.
  7. Direct messages. Out of scope here (separate subsystem).
  8. Explore / search. Algorithmic discovery of content from accounts the user does not follow; out of scope for the depth of this note (a separate ranking system).

2.2 Non-Functional Requirements

  1. Scale. ~3B monthly active users (Instagram reportedly crossed 3B MAU in September 2025, becoming the fourth platform after Facebook, YouTube, and WhatsApp to do so; it had reported ~2B MAU in late 2022); on the order of ~500M daily active users (DAU); ~95M+ photos/videos posted per day historically (numbers fluctuate). For sizing below we use 100M posts/day as a round design budget.
  2. Read latency. Feed render p99 < 200 ms (excluding image bytes); photo loading dominated by CDN edge speed.
  3. Upload latency. Photo upload acknowledged within ~2 s for the user; processed (resized into variants) within ~5 s.
  4. Availability. 99.95% on read; 99.9% on write.
  5. Durability. Photos and videos must never be silently lost. The mechanism is geographic replication and (for the warm tier) erasure coding — Haystack used RAID-6 plus ~3.6× geo-replication, and f4 cut that to ~2.1× with Reed-Solomon(10,4) while keeping the same durability guarantees (§8.1). The “eleven nines” (99.999999999%) durability target often quoted for this requirement is the canonical Amazon S3 design objective; the Haystack/f4 papers describe their replication scheme rather than publishing a specific nines figure, so treat “11 nines” as the modern industry bar, not a Haystack claim.

The “~95M photos/day” figure traces to circa-2018 statements and is not refreshed by any current Meta primary disclosure. The MAU figure, by contrast, is anchored: Instagram reportedly reached 3B MAU in September 2025 (Statista — Instagram global users), having reported ~2B in late 2022. Treat the posting-rate number strictly as a design budget — the design’s correctness depends only on its order of magnitude, not the exact value.

3. Capacity Estimation

3.1 Upload QPS

posts/day                = 100,000,000
seconds/day              = 86,400
average upload QPS       ≈ 1,160 uploads/s
peak upload QPS (5×)     ≈ 5,800 uploads/s

About 6K uploads/s at peak. Each upload is a multi-MB transfer plus metadata write plus async resize work — heavy.

3.2 Feed Read QPS

DAU                       = 5 × 10^8
feed reads per DAU/day   ≈ 30
feed reads/day            = 1.5 × 10^10
average read QPS          ≈ 1.5 × 10^10 / 86,400 ≈ 174,000 reads/s
peak (5×)                 ≈ 870,000 reads/s

Almost 1M feed-reads/s at peak. Each feed read returns ~20 posts; the photos themselves are fetched from CDN, so the application tier serves the list of post IDs and their metadata, not the bytes.

3.3 Image Storage

Per uploaded photo, multiple variants are generated (thumbnail, small, medium, original):

original (post-compression JPEG)  ≈ 500 KB average
thumbnail (200×200)               ≈ 30 KB
small (640×640)                   ≈ 100 KB
medium (1080×1080)                ≈ 250 KB
total per upload                  ≈ 880 KB ≈ 1 MB

Annual ingestion:

photos/year         = 10^8 × 365 ≈ 3.65 × 10^10 photos/year
storage/year        ≈ 3.65 × 10^10 × 10^6 bytes ≈ 36.5 PB/year
ten-year storage    ≈ 365 PB
3× replication      ≈ 1.1 EB (exabytes)

Plus video — much larger per asset (5–50 MB after compression), though far fewer videos than photos. Total storage is petabyte-scale today, exabyte-scale across the decade. This is the regime that motivated Haystack — purpose-built small-file storage rather than per-file filesystem inodes (which become an inode-overhead disaster at this volume).

3.4 Bandwidth

The image bytes served are the dominating bandwidth. Per feed read, ~20 photos × 100 KB (small variant served on mobile) = 2 MB. At 870K feed-reads/s peak, that’s 1.74 TB/s of image bandwidth — served from the CDN, not from origin. The origin’s bandwidth is just metadata responses (a few KB each), so origin bandwidth is tractable; the CDN bill is the real cost.

4. API Design

4.1 Upload (Two-Step Pattern)

Direct uploads of huge files through the application tier are wasteful (every byte traverses the API server). The standard pattern is presigned URLs:

POST /api/v1/uploads/init
Authorization: Bearer <JWT>
{
  "media_type":  "image/jpeg",
  "file_size":   524288
}
→ 200 OK
{
  "upload_id":     "upl_xyz",
  "presigned_url": "https://uploads.instagram.com/...&signature=...",
  "expires_at":    "2026-05-08T14:30:00Z"
}

The client then PUTs the bytes directly to the presigned URL (which writes to Amazon S3 or equivalent). Once complete, the client confirms the upload:

POST /api/v1/posts
Authorization: Bearer <JWT>
{
  "upload_id":  "upl_xyz",
  "caption":    "spring 2026",
  "location":   { "lat": 37.7, "lng": -122.4 },
  "hashtags":   ["#spring", "#sf"],
  "mentions":   ["@friend"],
  "alt_text":   "view of the bay"
}
→ 201 Created
{
  "post_id":      "1789234567890123456",
  "media_urls":   {
    "thumbnail":  "https://cdn.example/.../thumb.jpg",
    "small":      "https://cdn.example/.../small.jpg",
    ...
  },
  "created_at":   "2026-05-08T14:23:11Z"
}

The variants in media_urls may not all be ready at response time — typically the original is up immediately, the resized variants follow in seconds. Clients render whatever is available.

4.2 Feed Read

GET /api/v1/feed?count=20&cursor=<opaque>
→ 200 OK
{
  "posts": [
    {
      "post_id":   "1789...",
      "author":    { "user_id": "...", "username": "...", "avatar_url": "..." },
      "media":     [{ "kind": "image", "url_small": "...", "url_medium": "..." }],
      "caption":   "...",
      "like_count": 42,
      "comment_count": 7,
      "viewer_has_liked": false,
      "created_at": "..."
    },
    ...
  ],
  "next_cursor": "<opaque>"
}

4.3 Stories

GET /api/v1/stories
→ 200 OK
{
  "trays": [
    { "user_id": "...", "username": "...", "story_count": 3, "has_unseen": true },
    ...
  ]
}

GET /api/v1/stories/<user_id>
→ 200 OK
{
  "stories": [{ "story_id": "...", "media_url": "...", "expires_at": "..." }, ...]
}

5. Data Model

5.1 Posts (Metadata)

CREATE TABLE posts (
    post_id        BIGINT       PRIMARY KEY,    -- Snowflake-style time-ordered
    author_id      BIGINT       NOT NULL,
    caption        TEXT,
    location_lat   DOUBLE       NULL,
    location_lng   DOUBLE       NULL,
    hashtags       TEXT[]       NOT NULL DEFAULT '{}',
    mentions       BIGINT[]     NOT NULL DEFAULT '{}',
    media_count    SMALLINT     NOT NULL DEFAULT 1,
    created_at     TIMESTAMP    NOT NULL,
    is_deleted     BOOLEAN      NOT NULL DEFAULT FALSE,
    like_count     BIGINT       NOT NULL DEFAULT 0,        -- denormalized
    comment_count  INT          NOT NULL DEFAULT 0,
    INDEX idx_author_created (author_id, created_at DESC)
);

Sharded by author_id so a user’s profile page is a single-shard read.

5.2 Media Variants

Each post has 1+ media items; each item has multiple variants.

CREATE TABLE media_items (
    media_id     BIGINT      PRIMARY KEY,
    post_id      BIGINT      NOT NULL,
    position     SMALLINT    NOT NULL,             -- carousel ordering
    kind         VARCHAR(8)  NOT NULL,             -- 'image' | 'video'
    width        INT,
    height       INT,
    duration_ms  INT,                              -- video only
    INDEX idx_post (post_id)
);
 
CREATE TABLE media_variants (
    variant_id   BIGINT      PRIMARY KEY,
    media_id     BIGINT      NOT NULL,
    label        VARCHAR(16) NOT NULL,             -- 'thumbnail','small','medium','original'
    width        INT,
    height       INT,
    bytes        INT,
    storage_key  VARCHAR(512) NOT NULL,            -- 'haystack://volume_id:needle_id'
    cdn_url      VARCHAR(512),
    INDEX idx_media (media_id)
);

The storage_key format depends on the storage backend — for Haystack it’s volume_id:needle_id; for an S3-style store it’s s3://bucket/path. The CDN URL is the externally-served URL after the CDN has cached or fetched the bytes.

5.3 Social Graph (Follows)

Same dual-table structure as Twitter:

followers(user_id)  → set of follower_ids
following(user_id)  → set of followed_ids

For private accounts, an additional pending_follow_requests table holds requests awaiting approval.

5.4 Feed (Materialized Inbox)

Same Redis ZSET pattern as Twitter:

Key:    feed:<user_id>
Value:  ZSET of (post_id, score)
Cap:    ~500 entries

5.5 Stories (Ephemeral)

CREATE TABLE stories (
    story_id    BIGINT      PRIMARY KEY,
    user_id     BIGINT      NOT NULL,
    media_id    BIGINT      NOT NULL,
    created_at  TIMESTAMP   NOT NULL,
    expires_at  TIMESTAMP   NOT NULL,    -- created_at + 24h
    is_deleted  BOOLEAN     NOT NULL DEFAULT FALSE,
    INDEX idx_user_expires (user_id, expires_at DESC)
);

A scheduled GC sweeps expires_at < now() and deletes the story row + media variants. The same TTL pattern as Pastebin System Design.

6. High-Level Architecture

flowchart TB
    Client -->|HTTPS| Edge[CDN — image bytes from edge]
    Client -->|HTTPS| LB[L7 LB — API requests]
    LB --> WriteSvc[Post Write Service]
    LB --> ReadSvc[Feed Read Service]
    LB --> UploadSvc[Upload Service]
    UploadSvc -->|presigned URL| ObjStore[(Object Store / Haystack<br/>media bytes)]
    Client -.->|direct PUT| ObjStore
    WriteSvc --> PostsDB[(Posts DB<br/>sharded by author_id)]
    WriteSvc --> Kafka
    Kafka --> Resize[Resize / Transcode Workers]
    Kafka --> Fanout[Fanout Service]
    Kafka --> Moderation[Content Moderation ML]
    Kafka --> Search[Search Indexer]
    Resize --> ObjStore
    Resize -->|update variants| MediaDB[(Media Variants DB)]
    Fanout --> Graph[(Social Graph)]
    Fanout --> FeedInbox[(Feed Inbox<br/>Redis ZSET per user)]
    ReadSvc --> FeedInbox
    ReadSvc --> PostsDB
    ReadSvc --> MediaDB
    ReadSvc --> Counter[(Engagement Counters)]
    ReadSvc -->|build URLs| Edge
    Stories[Stories Service] --> StoriesDB[(Stories DB w/ TTL)]
    GC[Stories GC] --> StoriesDB
    GC --> ObjStore

What this diagram shows. The system has three critical paths. The upload path (top) uses presigned URLs so the client uploads bytes directly to object storage, not through the API tier — saving server bandwidth and isolating the heavy I/O from request-handling services. The post-creation path (left middle) writes metadata, then emits events to Kafka that drive resize/transcode workers (which produce the smaller variants), the fanout service (which delivers the post to followers’ feed inboxes), the content moderation pipeline (which runs ML inference to flag policy violations), and the search indexer (for hashtag and caption search). The feed read path (right) reads the user’s inbox of post IDs, fetches post metadata from the posts database and variant URLs from the media database, and returns a metadata-only response to the client — the actual image bytes are fetched by the client directly from the CDN. The stories subsystem (bottom) is mostly parallel: a separate database with TTL-based GC, but otherwise reuses the same object store and CDN. This separation of media-bytes from metadata is the architectural backbone of the design — the API tier never touches a single image byte.

7. Request Flow / Sequence

7.1 Upload and Post

sequenceDiagram
    participant C as Client
    participant U as Upload Service
    participant Obj as Object Store
    participant W as Write Service
    participant K as Kafka
    participant R as Resize Worker
    participant DB as Posts DB
    participant F as Fanout

    C->>U: POST /uploads/init {media_type, file_size}
    U-->>C: presigned PUT URL
    C->>Obj: PUT bytes (direct)
    Obj-->>C: 200 OK
    C->>W: POST /posts {upload_id, caption, ...}
    W->>DB: INSERT posts(post_id, author_id, ...)
    DB-->>W: ok
    W->>K: emit PostCreated {post_id, upload_id}
    W-->>C: 201 Created (with original media_url)
    par async — does not block client
        K->>R: consume event
        R->>Obj: GET original
        R->>R: resize → thumb, small, medium
        R->>Obj: PUT each variant
        R->>DB: UPDATE media_variants
    and
        K->>F: consume event
        F->>F: fanout to followers
    end

Two important design choices visible in this sequence. First, the client uploads bytes directly to the object store (not through the application server), saving infrastructure cost on the upload-bandwidth-heavy path. Second, the resize and fanout work happen asynchronously after the user’s request returns — the user gets 201 Created with the original variant available; smaller variants populate over the next few seconds. Clients display the original or a placeholder until variants are ready.

7.2 Feed Read

sequenceDiagram
    participant C as Client
    participant R as Read Service
    participant FI as Feed Inbox
    participant PDB as Posts DB
    participant MDB as Media DB
    participant CDN as CDN

    C->>R: GET /feed
    R->>FI: ZREVRANGE feed:<user_id> 0 19
    FI-->>R: [post_id_1, post_id_2, ...]
    par fetch metadata in parallel
        R->>PDB: SELECT * FROM posts WHERE post_id IN (...)
        R->>MDB: SELECT variants FROM media WHERE post_id IN (...)
    end
    R->>R: build response with CDN URLs
    R-->>C: JSON {posts: [...], next_cursor: ...}
    Note over C,CDN: client fetches image bytes directly
    C->>CDN: GET /<cdn_url for each variant>
    CDN-->>C: image bytes

The feed-read service returns metadata only; the client then fetches images directly from the CDN. This split is the source of Instagram’s read scalability — even at 1M feed-reads/s, the application tier sees only metadata-shaped traffic; the bandwidth-heavy image fetches go to the CDN, which is purpose-built for that workload.

8. Deep Dive

8.1 Photo Storage — Haystack and Beyond

Beaver et al.’s 2010 OSDI paper, “Finding a needle in Haystack: Facebook’s photo storage,” is the citation for small-file storage at social-network scale. The paper’s headline numbers (verified against the OSDI ‘10 text and summaries): Facebook stored over 260 billion images (~20 PB), users uploaded ~1 billion new photos per week (~60 TB), and the system served over 1 million images per second at peak (Beaver et al., OSDI 2010, USENIX; Murat Demirbas’s summary of the paper).

The problem: the prior NFS/NAS design, with one file per photo on a POSIX filesystem, required at least 3 disk operations to fetch an image — “one to read directory metadata into memory, one to load the inode into memory, and one to read the file contents” — and in the pathological case (deep directories, cold caches) as many as 10 (per the paper, via the summary above). At a billion photos/day with billions of long-tail (cold-cache) reads, that per-photo seek budget is what falls over first.

Haystack’s solution (paraphrasing Beaver et al. §3):

  • Store many photos in a single large “volume” file (a physical volume is “a very large file (100 GB)”); each photo within it is a “needle.”
  • Keep all metadata in main memory. A Store machine holds “an in-memory mapping of photo ids to the filesystem metadata (i.e., file, offset and size in bytes)” and open file descriptors per physical volume. So a photo read is a single disk seek (the read of the bytes) plus an in-memory lookup — no metadata seeks at all. This is the entire point of the design: trading the per-file inode walk for one RAM lookup.
  • Append-only writes. New photos append to the volume; deletions set a flag (tombstone) and the bytes are reclaimed later by an offline compaction pass.
  • RAID-6 at the node plus geographic replication for durability (the original Haystack replication factor was ~3.6×, later improved by f4 — see below).

The interview-takeaway: for huge numbers of small immutable objects, a custom blob store with in-memory indexing beats general-purpose filesystems and even general-purpose object stores — it collapses the 3-to-10 metadata-driven disk operations per read down to the single unavoidable seek for the photo bytes themselves.

Modern equivalents:

  • Facebook f4 (Muralidhar et al. OSDI 2014) — the warm BLOB tier beneath Haystack (Haystack stays the hot tier). f4 uses Reed-Solomon(10,4) erasure coding within a datacenter plus XOR coding across datacenters, dropping the storage replication factor from Haystack’s ~3.6× to ~2.1× while preserving durability against disk/host/rack/datacenter failures; the paper reports f4 storing over 65 PB of logical data in production (Muralidhar et al., f4, OSDI 2014, USENIX). The temperature insight — hot BLOBs are read constantly, warm BLOBs rarely — is what justifies a separate, cheaper tier.
  • Amazon S3 — works well enough at this scale that many companies skip the Haystack-style optimization and just pay for S3, accepting higher per-object overhead in exchange for not running storage infrastructure.
  • Cloudflare R2 / Backblaze B2 / Google Cloud Storage — same shape as S3.

For Instagram specifically: as part of Meta, Instagram benefits from Facebook’s Haystack/f4 stack. A standalone-Instagram interview answer typically substitutes S3 or an S3-compatible store for simplicity.

8.2 Image Resize and Transcode Pipeline

A photo upload triggers generation of multiple variants. The pipeline:

  1. Original is uploaded to object storage by the client via presigned URL. The post is created in the database with only the original variant URL populated.
  2. PostCreated event is emitted to Kafka.
  3. Resize workers consume the event. Each worker:
    • Fetches the original from object storage.
    • Generates 3+ resized variants using libvips, ImageMagick, or a custom GPU-accelerated path. libvips is preferred for memory efficiency on large images (it streams pixels rather than loading the whole image into RAM).
    • Encodes as JPEG (or AVIF/WebP for newer clients) at appropriate quality.
    • Uploads each variant to object storage.
    • Updates the media_variants table.
  4. CDN lazily fetches variants on first request, caches them at edge.

Idempotency: each variant is keyed by (media_id, label) so a worker retry produces the same key — no duplicates. Workers must be tolerant to “this work was already done” (no-op on detection) because Kafka guarantees at-least-once delivery.

For videos, the pipeline is similar but heavier: the worker transcodes to multiple bitrates (Adaptive BitRate / ABR) for HTTP Live Streaming (HLS) or DASH delivery. Transcode is CPU-intensive; a 1-minute video might take 10–30 seconds of CPU on a single core. Hence specialized GPU farms or hardware transcoders for high volume.

8.3 Feed Generation: Same Twitter Story, Almost

Instagram’s feed generation reuses the Twitter playbook (Twitter Newsfeed System Design §8) almost verbatim:

  • Fanout-on-write for the typical user: when a post is created, write the post_id into each follower’s feed inbox.
  • Fanout-on-read for celebrities: skip fanout; pull from the celebrity’s profile at read time.
  • Hybrid merge at read time when user follows celebrities.

The Instagram-specific twist: photos are heavier than tweets. The feed inbox stores only post_ids (tiny); the photo bytes are not duplicated per-follower. Without this discipline, fanout-on-write would consume terabytes per celebrity tweet.

Another Instagram specific: feed ranking. Instagram switched from chronological to algorithmic ranking earlier than Twitter (around 2016). The ranking model considers engagement signals (predicted likes/comments), recency, and personalized affinity. Implementation: a candidate set is assembled from inbox + pull, scored by an ML model at read time, then top-K returned. Caching of recently-computed rankings amortizes ML cost across a session.

8.4 Stories — Ephemeral Content

Stories add a 24-hour TTL to the post abstraction. Design choices specific to stories:

  • Storage in a separate database (not the main posts table). Stories’ high churn (95% deleted within 24 hours) is a different access pattern than posts (mostly retained forever). Mixing them inflates the posts table with rows that will be deleted within a day.
  • TTL-based GC. A scheduled job sweeps expires_at < now() and deletes story rows + media variants. Object-store-side TTL via S3 Lifecycle Policies is the common implementation.
  • Per-viewer “seen” tracking. The stories tray UI shows “you have unseen stories from X” — implemented via a per-(viewer, story) seen flag, stored densely (a bitmap or sparse table).

8.5 Direct Messages — A Different System

Direct Messages (DMs) are a separate subsystem in Instagram: a two-party (or group) conversation, end-to-end optionally encrypted, with read receipts. Architecturally closer to WhatsApp Messenger System Design than to the feed system. Out of scope for this note.

8.6 “Have I shown this user this post” Deduplication

When the feed ranker scores candidates, it should not re-show a post the user has already seen. Maintain a per-user “seen” set:

  • Bloom filter per user (see Bloom Filter) — fast probabilistic membership test, false positives possible (rare and acceptable: occasionally a post is hidden that the user hadn’t actually seen). Compact: a 100-bit Bloom filter holds ~1000 IDs at < 1% false positive rate.
  • Or: Redis SADD of seen post_ids per user, with TTL pruning.

The Bloom filter approach scales better at 2B users; the exact-set approach is more accurate per user but takes more memory.

Uncertain

Verify: that Instagram specifically uses a Bloom filter (vs. an exact Redis set or another structure) for “have I already shown this post” dedup. Reason: the technique is well-established in feed-ranking practice generally, but I found no Instagram/Meta primary source naming their concrete choice. To resolve: an Instagram engineering write-up on feed candidate-dedup. The general trade-off described here (probabilistic + compact vs. exact + memory-heavy) is sound regardless. uncertain

9. Scaling Strategy

9.1 What Breaks First

  1. Photo storage I/O in the naive “one file per photo” filesystem approach — Haystack’s motivating problem.
  2. Resize-worker queue lag. Bursty uploads (e.g., New Year’s Eve) can back up the resize queue. Solved by autoscaling workers and having the original immediately available so users see something while variants generate.
  3. Fanout queue on celebrity posts. Same as Twitter; same fix (pull-path for celebrities).
  4. Feed inbox writes. Sharded by user_id; hot users (verified accounts with many followers who post often) need their inbox split or otherwise spread.
  5. CDN cache miss rate on viral content. Even with edge caching, a viral post can produce tens of millions of cache misses globally during its first hour. CDNs handle this via tiered caching (regional → hierarchical → origin).

9.2 Sharding

Instagram’s published sharding scheme (Instagram Engineering, “Sharding & IDs at Instagram,” 2011/2012):

  • PostgreSQL, sharded by user_id. Each logical shard is a Postgres schema (the post uses insta5 in its examples), and each sharded table (likes, comments, photos) exists inside every schema. Many logical shards live on each physical database machine, so shards can be rebalanced by moving schemas between machines without changing IDs. The post’s worked example uses 2000 logical shards: for user_id 31341, the shard is 31341 % 2000 = 1341.
  • Custom 64-bit IDs that encode the shard. Instagram’s IDs embed the shard_id directly, so the ID itself tells you which shard holds the row — no lookup table needed.

The exact ID structure (verified from the post and a verbatim copy of it):

[41 bits time (ms since a custom epoch) | 13 bits shard_id | 10 bits sequence] = 64 bits
  • 41 bits of milliseconds since a custom epoch (1314220021721, ≈ 2011-09-09, with 2011-01-01 as the reference baseline) — giving ~41 years of IDs.
  • 13 bits of shard_id — room for 8192 logical shards (2¹³).
  • 10 bits of sequence — an auto-increment value taken mod 1024, so 1024 IDs per shard per millisecond; the sequence is generated by a per-schema Postgres stored function insta5.next_id().

Because the timestamp is in the most-significant bits, ORDER BY id equals ORDER BY created_at (time-sortable). And given an ID, you extract the middle 13 bits to learn its shard — turning “where is this row?” from a lookup into a bit-extraction. This is the key difference from Twitter’s Snowflake (which puts a datacenter+worker ID there, not a data shard): Instagram repurposed those bits for routing to storage.

9.3 Geo-Replication

Multi-region deployment with regional read replicas. Writes go to a primary region; reads served regionally. Object-store data is geo-replicated automatically by S3 (cross-region replication) or Haystack (cross-DC volume replication).

10. Real-World Example

Instagram’s actual stack (per Krieger 2011 blog, Instagram Engineering posts, and inheritance from Meta infrastructure):

  • Upload path. Mobile client uploads via HTTPS to S3 (originally) and now to Meta’s internal storage. Presigned URL pattern.
  • Photo storage. Originally S3 (pre-Facebook acquisition). After Facebook acquisition (2012), migrated to Facebook’s Haystack then f4. (Beaver et al. OSDI 2010; Muralidhar et al. OSDI 2014.)
  • Metadata storage. Originally PostgreSQL with manual sharding by user_id (Instagram Engineering 2012 “Sharding & IDs” post). Post-Facebook acquisition, social graph gradually migrated to Facebook’s TAO (Bronson et al. ATC 2013) — the Facebook-wide social graph store. Posts and metadata may use a mix of sharded PostgreSQL, Cassandra, and TAO depending on access pattern.
  • Cache. Memcached then Redis for hot reads.
  • Feed. Same hybrid push/pull as Twitter, with celebrity threshold.
  • Search. Elasticsearch for hashtag and caption search (Instagram engineering posts).
  • CDN. Akamai then mix of Akamai/CloudFront/Facebook’s own edge.

The TAO migration. TAO is Facebook’s distributed social graph store (Bronson et al., USENIX ATC 2013) — a two-tier write-through cache over a sharded MySQL backend, providing low-latency reads of social-graph queries (followers of, followed by, friends of friends). In TAO’s architecture, follower tiers serve reads locally while leader tiers coordinate writes and shield MySQL from thundering-herd read storms. Instagram’s social graph runs on TAO post-integration. TAO’s claim to fame: it handles over 1 billion reads per second at a 96.4% cache hit rate (note: the precise figure is 96.4%, not the loosely-quoted “99%”), with reads served in single-digit milliseconds, across petabytes of graph data on hundreds of thousands of shards.

Uncertain

Verify: the current breakdown of which metadata tables live in PostgreSQL vs. Cassandra vs. TAO. Reason: the 2011/2012 Instagram posts document the original sharded-Postgres design and the TAO/Cassandra adoptions are documented individually, but no primary source lays out the present-day allocation; the stack has evolved substantially post-2018 with little public documentation. To resolve: a current Meta/Instagram infrastructure disclosure. The component-level facts (Postgres origin, TAO for the social graph, Cassandra for some workloads) are each independently sourced; only the current composite is uncertain. uncertain

11. Tradeoffs

Design ChoiceOption AOption BWhen A winsWhen B wins
Upload pathThrough API serverDirect to object store (presigned URL)Small files, simplicityLarge files, common case
Photo storageGeneral object store (S3)Custom (Haystack)Want to outsource opsVolume justifies custom infra
Resize timingSynchronous (block upload response)Async (post-response)Need all variants immediatelyUX prioritized; first-paint matters
Resize variantsPre-generated all sizesOn-demand generationPredictable storage costLong tail of rare sizes
Feed strategyPure pushHybrid push/pullFew celebritiesWhales exist (Instagram case)
Feed capShort (100s)Longer (1000s)Most users read recentCatch-up reads important
Feed orderingStrict chronologicalML-rankedPredictability mattersEngagement matters
Stories storageSame DB as postsSeparate DB with TTLFew storiesHigh churn, want isolation
Photo IDRandom UUIDSnowflake with shard_idPure decentralizationWant shard-aware routing
Social graphCassandraTAO/MySQL+cachePure scale-outNeed follower/followed queries with cache locality
EncryptionAt rest onlyEnd-to-end (DMs)Public contentPrivate messages

12. Pitfalls

  1. Uploading large media through the API server. Wastes API-server bandwidth, ties up workers for the duration of upload. Always presigned-URL direct-to-storage.

  2. Synchronous resize on upload response. Users wait 5–30 s for response when most of that is server-side image processing. Async resize keeps the user-facing response fast.

  3. Non-idempotent resize workers. Kafka delivers at-least-once, so workers must be tolerant of re-running. Variant keys must be deterministic (function of media_id + label) so re-uploading produces the same key.

  4. Treating videos like photos in the storage layer. Videos are 50–500× larger; transcode is much more expensive; ABR streaming requires HLS/DASH manifest files in addition to media chunks. Build separate pipelines.

  5. Forgetting to clean up the original after resize. If you only ever serve the medium variant, the 5 MB original sitting in storage costs 50× more than necessary. Either keep all variants (current Instagram-style) or expire the original after a window.

  6. Same Twitter pitfall: pure fanout-on-write at celebrity scale. A 100M-follower celebrity’s photo causes 100M inbox writes. Hybrid push/pull is essential.

  7. Storing per-(user, post) seen set as a relational table at 2B users × 100K posts seen. That’s 2 × 10¹⁴ rows — infeasible. Bloom filters per user, or per-day Redis sets with TTL.

  8. Image-byte-bandwidth at the origin. Without CDN, the origin must serve every byte of every image to every viewer. CDN edge serving is non-optional at this scale.

  9. CDN cache key misalignment. If the URL contains user-specific tokens (e.g., signed CDN URLs with per-user expiration), the CDN cannot share cache across users — every user gets their own cache copy, defeating CDN economics. Use globally-cacheable URLs for public content; only sign URLs for genuinely-private content.

  10. Naive feed deduplication when reranking. The ranker may surface the same post twice if it appears in both inbox and pull paths (rare, but possible if a celebrity is mid-promotion to/from celebrity status). Dedupe by post_id during merge.

  11. Stories not actually expiring on time. If GC is delayed, stories linger past 24 hours — a privacy promise broken. Apply expiration at read time (filter by expires_at > now()) regardless of GC progress, so users see correct behavior even if storage cleanup lags.

  12. Failing to moderate uploaded content before fanout. A policy-violating image fanned out to millions of inboxes in seconds is hard to retract. Run content moderation ML inline (or in a fast async path that holds back fanout until the check completes for sensitive content).

  13. Allowing mention/hashtag spam to fan out to large hashtags’ followers. A spammer posting #popular #hashtag to artificially appear in popular hashtags’ search results — search must rank, not just include.

  14. EXIF metadata in images leaking GPS coordinates. Strip EXIF on upload by default unless the user explicitly opts in to location.

13. Common Interview Variants and Follow-Ups

  • “Add stories with progress bars.” Stories already designed; the per-story duration (15s for image, video length for video) and per-tray progress are client UI concerns mostly.
  • “Build the Explore page (algorithmic discovery).” A separate ranking system: candidate generation by recommender system over the user’s interest signals, then feature-store-backed ML scoring at read time. Different from the feed because candidates are not from followed accounts.
  • “Add direct messages.” Different system; closer to WhatsApp Messenger System Design than to feed.
  • “Add live streaming (Instagram Live).” Real-time video streaming infrastructure; closer to Twitch Live Streaming System Design.
  • “Make it work offline (cached feed for the subway).” Client-side cache; on reconnect, sync incremental updates. Conflict resolution rare since reads only.
  • “Detect copy of someone else’s photo.” Perceptual hashing (pHash) on upload; lookup against a hash index for known-stolen content. Fingerprinting infrastructure.
  • “Now make it private-by-default with end-to-end encryption.” All photos encrypted client-side; server stores ciphertext only. Server-side ML moderation becomes impossible (sees nothing); rely on client-side or report-based moderation.
  • “Estimate how many resize workers you need.” 6K uploads/s × 0.1 CPU-seconds per resize = 600 CPU-cores needed for steady-state resize. Plus headroom for peaks → ~2000 cores → ~250 16-core machines.
  • “How do you backfill a follow?” Lazy: only future posts. Eager: queue a backfill task that reads the followed user’s recent N posts and prepends to the follower’s inbox. Eager is more user-friendly.

14. Open Questions / Uncertain

One earlier flag is resolved: the MAU figure is now anchored — Instagram reportedly crossed 3 billion MAU in September 2025 (it had reported ~2B in late 2022), so the note now sizes against 3B rather than a vague “2B-ish” (Statista). The Haystack, f4, and TAO mechanisms cited above are each confirmed against their primary OSDI/ATC papers (see §8.1 and §10).

The following remain genuinely open:

Uncertain

Verify: that Instagram still uses Haystack/f4 specifically in 2026 (vs. a successor BLOB store at Meta). Reason: Haystack (OSDI ‘10) and f4 (OSDI ‘14) are well-documented, but their successors are not publicly documented. To resolve: a current Meta storage-infrastructure disclosure. uncertain

Uncertain

Verify: the exact follower threshold at which Instagram switches from push to pull fanout. Reason: not publicly disclosed; the hybrid strategy is sound (and matches Twitter’s documented approach), but any specific number is inferred. To resolve: an Instagram feed-architecture write-up. uncertain

Uncertain

Verify: the “have I shown this post” dedup data structure (Bloom filter vs. exact set) — see §8.6. Reason: no Instagram primary source names the concrete choice. uncertain

15. See Also