30-Second Glance
- What: Photo/video sharing — upload pipeline, object storage + CDN media serving, home feed reusing News Feed patterns, 24 h stories.
- Scale: ~100M photos/day (~2 MB avg ⇒ ~200 TB/day ingress); 10B+ media views/day served ~95% from CDN edge.
- Core trick: Bytes never touch origin synchronously (presigned direct upload + async variants); feed = News Feed case hybrid with media hydration; stories = TTL'd hot store + hard delete.
- Hardest part: Upload reliability at 100M/day (resume, dedup, exactly-once metadata); CDN cost/invalidation; stories expiry at scale.
1. Requirements
Functional:
- Upload photo (≤ 20 MB) / video (≤ 4 GB, ≤ 60 s reels longer async) with caption, filters client-side.
- Home feed (ranked, cursor pages) + profile grid + stories tray + reels tab.
- Stories: 24 h TTL, viewers list, replies → DMs.
- Like/comment/follow; counts approximate.
- Basic safety: NSFW/CSAM hashing + takedown within minutes.
Non-functional:
| Requirement | Target |
|---|---|
| Upload-accept p99 | < 500 ms (metadata accept; bytes stream direct) |
| Media first-byte p99 | < 200 ms warm edge, < 800 ms cold |
| Feed read p99 | < 200 ms (inherits News Feed SLO) |
| Availability | 99.95% reads; uploads retryable (client-resumable) |
| Durability | 11×9s object durability (S3-class); metadata RDBMS replicated |
Out of scope:
- Client-side filter rendering engine.
- Full messaging stack (stories replies hand off to DMs).
- Live streaming (separate infra).
2. Capacity Estimation
Assumptions: 2B users, 500M DAU; 100M photos/day (2 MB avg) + 10M videos/day (30 MB avg); 3 stored variants per photo (~2.6 MB total); 10B media views/day (avg served 500 KB after variant selection); peak 3×.
| Metric | Math | Result |
|---|---|---|
| Photo ingress | 100M × 2 MB | ~200 TB/day |
| Video ingress | 10M × 30 MB | ~300 TB/day (dominant! — drives async transcode design) |
| Variant storage / year | (100M × 2.6 MB + 10M × ~45 MB renditions) × 365 | ~260 PB/year — object storage only; never disks on app hosts |
| Egress (views) | 10B × 500 KB | ~5 EB/day — must be ~95% CDN edge; origin serves ~250 PB/day (still huge ⇒ origin shield + tiered cache) |
| Metadata writes | 110M media rows + ~1B likes/comments per day | ~13k/s avg — sharded MySQL/Cassandra territory |
| Stories steady-state | 400M stories/day × 1 MB × 1 day TTL | ~400 TB hot, self-cleaning (§6.3) |
3. API Design
POST /api/v1/media/upload-slot
{ "bytes": 2100000, "mime": "image/jpeg", "checksum_sha256": "…" }
201 { "media_id": "m_9f2Kd",
"upload_url": "https://uploads.ig.example/m_9f2Kd?sig=…", // presigned, 15-min
"upload_id": "u_77" } // multipart session for video
PUT {upload_url} // bytes go direct to object storage, resumable chunks
POST /api/v1/media/m_9f2Kd/commit
{ "caption": "…", "filter": "clarendon", "checksum_sha256": "…" }
202 { "media_id": "m_9f2Kd", "status": "PROCESSING" }
GET /api/v1/feed?limit=20&cursor=... // same contract as News Feed
GET /api/v1/stories/tray // active (unexpired) stories of followees
GET /api/v1/media/{id} → 302 to CDN-signed variant URL (size negotiated by client hints)| Endpoint | Purpose | Notes |
|---|---|---|
| upload-slot | Reserve id + presigned URL | Idempotent on client mutation-id; checksum required (dedup hook) |
| chunk PUT / commit | Byte transfer + finalize | Multipart for video; commit validates checksum → exactly-once metadata insert (§6.1) |
| feed / stories tray | Reads | Feed = News Feed contract; stories served from hot TTL store |
| media GET → 302 | Variant redirect | Never proxy bytes; 302 to versioned CDN URL (…/m_9f2/v3_1080.jpg) so updates = new version, no purge |
- Client picks variant by
Save-Data/ viewport hints (?w=1080); responseVary+ separate edge keys per width bucket (3 buckets, not infinite). - Webhooks/polling for
PROCESSING → READY(video):GET /media/{id}/statuswith 60 s client backoff.
4. Data Model & Storage
| Entity | Store | Partition / shard key | Why this store |
|---|---|---|---|
| Media bytes + variants | Object storage (S3-class) | media_id prefix | 11×9s durability, infinite scale, CDN origin |
Media metadata (owner, caption, variant URLs, status, expires_at?) |
Sharded MySQL (or Cassandra) | media_id (hash) | Structured + secondary index on (owner_id, created_at) for profile grids |
| Home feed ids | Redis zsets | user_id | Reused News Feed L1 verbatim |
| Stories | Redis/Cassandra hot with TTL = expires_at + object storage | (owner_id, story_id) | Self-expiring; viewers-list in Redis set (cap 10k shown + count) |
| Social graph / counters | Same as News Feed / Twitter cases | — | No new primitive needed (say this in the interview) |
| Safety hashes (PhotoDNA-style) | Dedicated KV + blocklist | content hash | Checked at commit, before fan-out |
-- media_metadata (media_id PK, owner_id, type, status, caption,
-- variants JSON {thumb,feed,full,hls...}, bytes, checksum UNIQUE,
-- created_at, expires_at NULL, INDEX (owner_id, created_at DESC))
-- stories ((owner_id), expires_at, story_id) WITH TTL semantics + viewer countschecksum UNIQUEgives free dedup: re-upload of the same bytes returns the existingmedia_id(quota + storage win).- Versioned variant URLs (
…/v3_…) ⇒ CDN invalidation is a non-problem for edits; takedowns use purge-by-prefix (rare, fine).
5. High-Level Design
Upload: app → upload-slot (id + presigned URL) → client PUTs bytes direct to object storage → commit (checksum verify, metadata INSERT, safety-hash check) → workers (variants/transcode/ML/embeddings) → READY → fan-out id into feeds (News Feed hybrid). Read: feed → hydrate media metadata → 302/client-direct to CDN variant URLs; stories: tray → hot TTL store → CDN.
- Upload service: slot/commit/status only — never handles bytes (stays stateless + tiny).
- Object storage + CDN: bytes plane; origin shield in front of buckets; 3 width buckets cached at edge.
- Worker fleet (async): thumbnail/transcode, NSFW/safety, embeddings for ranking/ads, stories-expiry sweeper.
- Metadata DB (hash-sharded): the source of truth clients hydrate from.
- Feed + stories read path: News Feed hybrid for posts; TTL hot store for stories; viewers-list writes buffered (Kafka → rollup, cap displayed list).
6. Deep Dives
6.1 Upload pipeline: resume, dedup, exactly-once metadata
Flow with failure handling at each step: (1) upload-slot is idempotent on (user, client_mutation_id) — retries return the same URL. (2) Bytes transfer is multipart with per-part ETags; client resumes from the server-reported received-parts list (no restart on flaky mobile). (3) commit verifies SHA-256, then INSERT ... ON CONFLICT(checksum) DO NOTHING — double-commit returns the existing row (exactly-once effect without distributed transactions; note the Atomic Commit and Two-Phase Commit (2PC) alternative and why it's overkill here). (4) Safety-hash check runs before fan-out but after accept — offending media is quarantined (status BLOCKED), never fanned out, reported within minutes. (5) Orphan sweeper: slots with bytes but no commit after 24 h are GC'd (multipart abort + delete). Poison media (worker crash-loop) → dead-letter + FAILED status surfaced to the client with retry.
6.2 CDN strategy: variants, versioning, cost control
- Variant ladder: photo → thumb (150px, ~30 KB), feed (1080px, ~400 KB), full (original); video → HLS 3–5 renditions. Edge caches only the 3 photo buckets + top 2 video renditions; tail renditions collapse to origin shield (hit ratio still >90% by bytes because the head dominates).
- Versioned URLs kill invalidation: edit/re-encode ⇒ new version path; old edge entries expire naturally. Purge reserved for takedowns (purge-by-prefix, audited, rate-limited — a purge storm is a self-DDoS).
- Cost levers (name 2–3 in the interview): AVIF/WebP negotiation (~30% bytes saved),
Save-Datadowngrade ladder, prefetch only first-viewport media, P2P/off-peak prefetch for drafts — none. Egress dominates the bill, so every 10% byte saving beats any origin optimisation. - Signed URLs for private accounts/stories (
exp=15 min, minted per viewer) vs public long-TTL edge caching — two policies, enforced at the 302 step, not in CDN config.
6.3 Stories TTL at scale + feed reuse statement
- Stories rows carry
expires_at = created_at + 24 h; hot store (Redis/Cassandra) applies native TTL so expiry is deletion by clock, not by job. A daily sweeper only reconciles (object-storage delete for variants + viewers-list GC + safety retention holds) — the read path never sees expired rows because every read filtersexpires_at > now(defence in depth against clock/GC lag). - Viewers-list:
SADD story:{id}:viewerscapped — past 10k, store count only (HyperLogLog + exact head). Writes buffered via Kafka; story owner sees ~second-fresh counts, fine. - Feed reuse (say explicitly): home feed, fan-out hybrid, celebrity pull, cache tiers, cursor pagination are inherited unchanged from the News Feed case — Instagram's novelty is the media plane + stories lifecycle, not a new feed algorithm. Reels = same feed with a video-ranked scorer + prefetch of first video bytes.
7. Bottlenecks, Trade-offs & Monitoring
First bottleneck at 10x: video transcode fleet (300 TB/day ingress ⇒ GPU/CPU burn) → admission control (cap concurrent transcodes per user), prioritise by predicted reach (celebrity videos first), downgrade ladder under backlog (ship 720p now, 4K later). Next: CDN egress bill → format + prefetch discipline (§6.2). Next: metadata DB write heat on viral posts' comment threads → comment pagination + counter batching.
| Approach | Pros | Cons | Use when |
|---|---|---|---|
| Bytes via origin | Simple auth/progress | Bandwidth + connection death | Prototype |
| Presigned direct + async (chosen) | Infinite byte scale, cheap | Eventual READY, client complexity | Production |
| Versioned URLs (chosen) | No invalidation storms | Old versions linger to TTL | All media |
| Purge-on-edit | Immediate | Self-DDoS at scale | Takedowns only |
| Metric | Alert if |
|---|---|
| Upload commit→READY p50/p99 | > 30 s / > 5 min (transcode backlog) |
| Orphan-slot rate | > 1% (client or commit bug) |
| CDN edge hit ratio (bytes) | < 90% (variant/config regression = money fire) |
| Stories post-TTL visibility | any (correctness bug — page) |
| Feed read p99 | > 300 ms (inherits News Feed runbook) |
Interview one-liners
- Q: Where do upload bytes flow? / A: Never through origin — presigned direct-to-object-store, resumable; origin only handles slot/commit metadata.
- Q: How do stories disappear? / A: Native TTL + read-time expiry filter; a sweeper only reconciles bytes and viewer lists.
- Q: New feed algorithm for Instagram? / A: No — reuse the News Feed hybrid; Instagram's hard parts are the media plane and stories lifecycle.
Related
- News Feed case — prerequisite: feed, fan-out, ranking, cache tiers
- Twitter Timeline case — sibling: media/graph side-paths, home vs user timelines
- Cache Patterns and Eviction — CDN + edge variant caching
- Partitioning — metadata sharding by media_id
- Atomic Commit and Two-Phase Commit (2PC) — why commit uses idempotent insert instead
- Stream Processing — transcode farm + viewers-list rollups
- Relational Vs Non-Relational DBs — metadata store reasoning