SD System Design In Depth
0%
case system-designcasescalinginstagram

Case: Instagram's first architecture

Module 6.4: Real-World Architecture Evolution & Incidents Updated Sep 18, 2026
📄 Cheat Sheet
Storage Systems at a Rapidly Scaling Startup with a Small Team - Data@Scale
Meta Developers Storage Systems at a Rapidly Scaling Startup with a Small Team - Data@Scale Watch on YouTube ↗

Mike Krieger's own Data@Scale talk on how Instagram's tiny team chose and scaled its storage (PostgreSQL sharding, Redis, memcached, S3), the exact early-architecture story of this unit.

0:00 / --:--

Case: Instagram's First Architecture

Instagram's early scaling work is legendary because it was achieved by an incredibly small team. It separated three jobs that appear together on a phone: running application code, retrieving records, and delivering image bytes. Its engineers' 2011 posts and Mike Krieger's 2012 talk let us follow those choices.

When Instagram launched in 2010, they grew from 0 to 1 million users in just a few months, with only a handful of engineers. They couldn't afford complex microservices; they needed extreme simplicity and reliable off-the-shelf tools.

The Core Stack: Keep It Simple

The original architecture was built entirely on Amazon Web Services (AWS) using technologies that were boring and predictable:

  • OS: Ubuntu Linux
  • Load Balancing: Amazon Elastic Load Balancer (ELB) routing requests to Nginx.
  • Application Server: Django running on Python with Gunicorn.
  • Data Store: PostgreSQL for user data and metadata, Redis for feeds and sessions, Memcached for caching.
  • Storage: Amazon S3 for photos, CloudFront for CDN.

Scaling the Database: Sharding IDs

The most fascinating part of early Instagram was how they scaled PostgreSQL. As they grew rapidly, a single database could no longer hold all the data or handle the read/write volume.

They needed to partition (shard) the data across multiple database instances. But this introduced a problem: how do you generate unique IDs for photos when you don't have a single central database keeping track of the auto-incrementing primary key?

Their solution was brilliant and simple. They created a custom ID generation scheme using PL/pgSQL directly inside the database shards. The 64-bit ID consisted of:

  • 41 bits for time in milliseconds (about 69.7 years from the chosen custom epoch).
  • 13 bits that represent the logical shard ID.
  • 10 bits that represent an auto-incrementing sequence, local to each schema (modulo 1024).

This allowed the application to route a request directly to the correct shard just by inspecting the photo ID, without needing a per-ID lookup table to find where the photo lived; a small map from logical shards to physical servers completes the routing.

flowchart TD Client["Mobile App"] --> ELB["Load Balancer"] ELB --> App1["Django Server"] ELB --> App2["Django Server"] App1 --> S3["Amazon S3"] App1 --> Redis["Redis Clusters"] App1 --> Postgres[("Postgres Shards")]

Storing the Feed in Redis

To serve the home feed quickly, Instagram heavily utilized Redis. Whenever someone posted a photo, the photo ID was pushed into the Redis lists of all their followers. Because Redis keeps all data in memory, pulling a user's feed was instantaneous.

To save memory, they didn't store full JSON objects in Redis, only the photo IDs. The application would fetch the IDs from Redis, and then do a quick SELECT IN query against PostgreSQL to get the photo metadata (like caption and author).

Worked example: building and decoding an Instagram ID

Layout: 41 bits of milliseconds since a custom epoch, 13 bits of logical shard id, 10 bits of per-shard sequence.

  • Time since epoch: 1,000,000,000 ms (about 11.6 days).
  • Logical shard: 1341 (13 bits allows 8,192 logical shards).
  • Sequence: the shard's sequence returns 5001, and 5001 mod 1024 = 905.

ID = (1,000,000,000 shifted left by 23) OR (1341 shifted left by 10) OR 905 = 8,388,608,000,000,000 + 1,373,184 + 905 = 8388608001374089. Decoding: shift right by 23 to recover the timestamp; shift right by 10 and mask with 8191 to recover shard 1341. The application then looks up which physical PostgreSQL server currently hosts logical shard 1341.

Limits: 1,024 IDs per shard per millisecond, and 41 bits of milliseconds last about 69.7 years from the chosen epoch.

The alternatives Instagram rejected

OptionProsCons for Instagram
UUIDsNo coordination128 bits; not time sortable; poor index locality
Twitter Snowflake serviceTime sortable 64-bit IDsAnother service to run (and ZooKeeper) for a tiny team
Ticket database (Flickr style)SimpleA single write bottleneck and point of failure
PL/pgSQL function in each shard schemaNo new service; time sortable; shard embeddedRelies on per-shard sequences and reasonably synced clocks

Why logical shards matter

Instagram created thousands of logical shards as PostgreSQL schemas and placed many on each physical server. Moving load meant moving whole schemas to a new server (for example by promoting a replica) and updating the logical-to-physical map, without ever re-hashing a single ID. This is the same "many more partitions than nodes" idea used by Cassandra vnodes and Kafka partitions.

Feed storage trade-off

Pushing each new photo id into every follower's Redis list (fan-out on write) makes reading a feed a single list read plus a batched metadata lookup, but a user with millions of followers turns one post into millions of writes. Large systems eventually mix fan-out on write for most users with fan-out on read for very high follower counts.

Check yourself Question 1 of 3
Routing by photo ID recall
From: Case: Instagram's First Architecture

Instagram's 64-bit photo IDs embed a 13-bit logical shard number. Given a photo ID, how does the application find the PostgreSQL server that holds the photo?

Key Takeaways

Keep the architecture boring and simple; avoid complex, unproven technologies when scaling rapidly.
Separate the heavy lifting (image delivery via CDN) from application logic (Django) and metadata (Postgres).
When sharding databases, embedding a logical shard ID into the primary key replaces per-row lookups with a small logical-to-physical shard map, which is also what makes rebalancing possible.
Use in-memory data stores like Redis for highly read-heavy, low-latency data structures like user feeds.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue