Skip to content
System design course
Ch.4 · Designing real systems·how to build it ·7 min read

Building Instagram

Implement direct-to-blob uploads with pre-signed URLs, async image-variant generation, the feed assembly reusing fan-out, and CDN-served images.


The upload path (direct to blob, async processing)

Clients upload large media directly to the object store via a pre-signed URL, so app servers never proxy bytes; processing happens async:

def request_upload(user_id):
    media_id = uuid()
    key = f"uploads/{user_id}/{media_id}"
    return {"upload_url": blob_store.presign_put(key, ttl="10m"), "media_id": media_id}

def finalize_post(user_id, media_id, caption):
    key = f"uploads/{user_id}/{media_id}"
    post_id = snowflake()
    posts.put(post_id, {"user": user_id, "caption": caption,
                        "media_key": key, "ts": now(), "status": "processing"})
    media_queue.publish({"post_id": post_id, "key": key})   # async variant generation
    return post_id

Image-variant generation (async worker)

Generate the sizes the feed needs once, so reads never resize:

VARIANTS = {"thumb": 150, "feed": 1080, "full": 2048}
def media_worker(msg):
    original = blob_store.get(msg["key"])
    urls = {}
    for name, width in VARIANTS.items():
        img = resize(original, width)                # + transcode video to HLS segments
        out_key = f"media/{msg['post_id']}/{name}.webp"
        blob_store.put(out_key, img)
        urls[name] = cdn_url(out_key)
    posts.update(msg["post_id"], media_urls=urls, status="ready")
    fanout_queue.publish({"post_id": msg["post_id"], "user_id": author_of(msg)})  # then fan out

Fan-out happens after processing, so a post appears in feeds only once its images are ready.

Feed assembly (reuse the fan-out)

Identical to Twitter — push post ids into followers’ feeds (hybrid for celebrities), read the precomputed list, hydrate, and attach CDN image URLs:

def home_feed(user_id, k=20):
    ids = redis.lrange(f"feed:{user_id}", 0, k)      # precomputed (pushed) post ids
    ids += pull_celebrity_posts(user_id, k)          # merge big accounts at read time
    posts_ = hydrate(top_by_time(ids, k))            # batch-fetch metadata
    return [{**p, "image": p.media_urls["feed"]} for p in posts_]   # CDN url, right size

Serving images

The client requests the right variant for its context (thumb in a grid, feed size in the feed, full on tap) directly from the CDN — so the app/DB never touch image bytes, and users get appropriately-sized, edge-cached images.

Storage and counts

  • Media in the object store (erasure-coded) + CDN — the dominant cost; dedup identical uploads by content hash.
  • Posts/graph/feeds sharded as in Twitter.
  • Likes/comments counts → async approximate counters (incrementing a DB row per like at scale is a hotspot); comment lists paginate from a store.

Failure handling and edge cases

  • Upload fails mid-way → pre-signed PUT is idempotent by key; a sweeper cleans orphaned uploads with no finalized post.
  • Processing fails → retry; the post stays processing (not yet in feeds) until variants exist or it’s dead-lettered.
  • Celebrity post → pull-and-merge (no fan-out storm).
  • Hot image → CDN absorbs it entirely.
  • Feed cache loss → rebuild from followees’ recent posts.

The takeaway

Concrete signals: direct-to-blob pre-signed uploads, async variant generation (resize/transcode once), CDN-served right-sized images, fan-out reused from Twitter, and approximate counters. The media plane (blob + CDN + variants) layered on the feed pattern is exactly how every photo/video social app is built — TikTok and the news feed are variations.