Upload and serve billions of photos with an object store + CDN, sharded Postgres for metadata, and time-sortable IDs generated inside the database.
Instagram is upload-a-photo, then serve it to followers' feeds — billions of times. The core design tension is that a photo has two very different parts: the bytes (large, immutable, served worldwide) and the metadata (small, relational: caption, author, likes). They want completely different homes.
Take 100M photos/day uploaded, averaging ~200 KB–2 MB each. Even at 500 KB that is ~50 TB/day of new media — far too much to sit in a relational database, and it must be served with low latency to users on every continent. Metadata, by contrast, is tiny: a few hundred bytes per photo.
Core data lives in sharded PostgreSQL (Instagram chose relational over NoSQL for it). There are several thousand logical shards — each a Postgres schema — mapped onto far fewer physical machines, and a row's shard is picked by `id % N`. The clever part is the 64-bit ID, generated *inside* Postgres with no separate ID service:
64-bit ID layout:
[ 41 bits ] milliseconds since a custom epoch -> IDs sort by time
[ 13 bits ] shard id (which logical shard) -> the ID encodes its own home
[ 10 bits ] per-shard sequence (1024 ids / ms) -> uniqueness within the ms
Because the shard is baked into the ID, you scale by MOVING logical shards
between machines, never by re-bucketing individual rows.To assemble a home feed you can either fan-out on write (when you post, push the post's ID into each follower's precomputed feed list) or fan-out on read (at read time, query recent posts from everyone you follow and merge). Fan-out-on-write makes reads cheap but writes expensive; fan-out-on-read is the reverse. A follows table (follower_id, followee_id) drives either path. Slow follow-on work — cross-posting, push notifications — is pushed to a task queue (Instagram used Gearman with ~200 workers) so the upload returns fast.