Skip to main content
PProfileMatch

MediumProblem 6 of 36 in the system design series

Design Instagram: Photo Sharing with Hybrid Feed Fan-out

Prefer watching? This article is the written companion to the video above, from the Shubham Aher YouTube channel.

A photo sharing app lets people publish pictures and short videos, follow accounts they find interesting, and scroll a home feed assembled from everyone they follow. The surface looks modest, but each scroll of that feed asks the backend to combine posts from hundreds of authors and deliver images that load instantly whether the viewer is in Pune or Paris.

Interviewers reach for this problem because it contains two classic challenges side by side. The first is media: hundreds of terabytes of new photos and videos arrive daily and must be stored durably and served quickly worldwide. The second is the feed: building a personalised timeline for every user is easy at small scale and surprisingly subtle once a single account has hundreds of millions of followers.

The heart of a good answer is deciding when to do the feed work. Doing it while writing makes reads cheap but turns a celebrity post into a flood of writes; doing it while reading avoids the flood but makes every feed load expensive. The design below for an Instagram-like service blends both and explains exactly where the line falls.

This is an interview practice design for a Instagram-like service, built from first principles. It is not a description of how Instagram is actually built.

Functional Requirements

Three features matter. Users create a post containing a photo or short video plus a caption. Users follow other accounts, and the relationship is one-directional: a fan can follow a famous athlete without the athlete following back. That asymmetry is what produces the extreme follower counts that complicate the feed later.

Users also view a home feed of recent posts from the accounts they follow. To keep the scope tight, the feed is chronological, newest first, rather than ranked by a model. Stories, direct messages, comments and machine-learned ranking are all real features, but each could fill its own interview, so they are set aside here.

Create a post

Upload a photo or short video with a caption.

photo · video

Follow users

Follow anyone; they don't have to follow back.

one-way

View the feed

Recent posts from people you follow, newest first.

chronological

  • Out of scope today: stories, DMs, comments, ML ranking

Non-Functional Requirements

The home feed should load in under 500 ms, because it is the very first screen after opening the app and any delay is felt by every user, every session. Media must also load fast globally, which in practice means serving it from edge locations near viewers rather than from one central region.

Availability takes priority over strict consistency. If a friend's new photo reaches your feed a few seconds after they post it, you will never notice, but an app that refuses to load the feed during a partial outage is immediately noticed. Finally, assume massive scale, around 500 million daily active users, which rules out any approach that does heavy per-request work on a single database.

Fast feed

The first screen everyone sees must load instantly.

< 500 ms

Fast media everywhere

Photos load quickly in every country.

CDN

Availability first

A post reaching feeds a few seconds late is fine.

eventual consistency

Massive scale

Hundreds of millions of users every day.

500M DAU

Scale Estimation

Assume 500 million daily active users creating about 100 million posts per day, roughly 1,150 posts per second. Assuming the typical person checks their feed around ten times daily, that yields about 5 billion feed reads per day, near 58K per second on average.

With an average of about 2 MB of media per post, new uploads total roughly 200 TB per day, which belongs in object storage rather than any database. The most important ratio is between feed reads and posts: reads outnumber posts about fifty to one. That pushes the design toward doing preparation work when a post is created so that each of those far more frequent feed loads becomes a cheap lookup.

500million

Daily active users

≈ 10 feed opens each / day

100million

New posts per day

≈ 1,150 per second

58K avg

Feed reads per second

5 billion reads / day

200TB

New media per day

100M posts × 2 MB

Feed reads ≈ 50 × posts → make reading the feed cheap

Core Entities & API

The data model has four entities: users, posts, follow relationships, and the media objects attached to posts. Keeping media separate from the post record lets the heavy bytes live in object storage while the post row stays small.

Publishing is POST /posts with a caption and the media type. The response returns a post_id and an upload URL, so the client sends the photo or video directly to storage rather than through our API. Following is POST /users/{id}/follow, returning 200 OK. The feed is GET /feed?cursor={c}, which returns a page of posts and a next_cursor. A cursor is better than page numbers here because new posts keep arriving at the top, and offset-based pages would shift and show duplicates.

Core entities

UserPostFollowMedia

API

  • POST /posts { caption, media_type }→ { post_id, upload_url }
  • POST /users/{id}/follow→ 200 OK
  • GET /feed?cursor={c}→ { posts[], next_cursor }

High-Level Design

Clients reach an API gateway for authentication and routing. Publishing starts with a request to the post service for an upload URL, after which the app pushes the bytes directly into object storage such as S3. The post record itself, holding the caption and a pointer to the media object, goes into a posts database; Cassandra suits this well because the workload is write-heavy, append-only and naturally partitioned by author. Viewers load media from a CDN that caches objects near them, so our servers never stream image bytes.

Each new post emits an event to Kafka. A fan-out service consumes those events and has workers push the new post ID onto the cached timeline of each follower in Redis. When a user opens the app, the feed service fetches the cached ID list for that person and hydrates them with post details. The guiding principle is that the expensive assembly happens once at write time, which turns the frequent feed read into a fast key lookup.

upload, directsave postview medianew postpush IDread timelineClientapp · webAPI gatewayauth · routingPost servicecreate postsPosts DBCassandraObject storageS3 · photos, videosCDNedge cache of S3Fan-out serviceKafka · workersFeed cacheRedis · post IDsFeed serviceread timelines
High-level design. Dashed arrows are taken only on a cache miss or as a fallback. Scroll sideways on small screens.

Do the work at write time → reading the feed is a cheap lookup

Deep Dive 1 — Media Upload & Delivery

The client begins by requesting a presigned upload URL from the post service, so photo and video bytes never pass through application servers. Large videos use multipart upload, which splits the file into parts sent independently; if the network drops, only the failed part is retried instead of the whole file.

When an upload completes, S3 emits an event that triggers background workers. For images, they generate thumbnails and several resized versions so a phone on a small screen downloads a compact image, not a 12-megapixel original. For videos, workers transcode into multiple bitrates packaged for adaptive streaming such as HLS, letting the player step down to a lower quality on a weak connection instead of stalling.

Every processed file is immutable, which is ideal for caching. The CDN can store these objects with very long expiry times, and the overwhelming majority of media requests are answered at the edge without touching S3 at all. That keeps image loads fast worldwide and keeps origin bandwidth costs under control.

  1. 1Get an upload URLPost service returns a presigned S3 URL
  2. 2Upload directlyMultipart upload; a failed part is retried alone
  3. 3Resize in backgroundS3 event → workers make thumbnails and sizes
  4. 4Transcode videoSeveral qualities with adaptive streaming (HLS)
  5. 5Serve via CDNFiles never change → cache them for a long time
  • Most media requests are answered by the CDN and never reach S3

Deep Dive 2 — Feed Fan-out

Fan-out on read assembles a feed at request time by querying recent posts from every followed account and merging them. Posting is cheap, but a user following 500 accounts triggers a large multi-partition query on every feed open, and at 58K reads per second that becomes the dominant cost.

Fan-out on write does the opposite: when someone posts, the system inserts the post ID into each follower's precomputed timeline, so reading is a single lookup. The weakness is extreme accounts. One post from an account with 200 million followers would require 200 million timeline writes, delaying delivery for everyone and wasting effort on followers who rarely log in.

The hybrid keeps fan-out on write for ordinary accounts and skips it for accounts above a threshold, for example around 1 million followers. When a user opens the feed, the service reads their precomputed timeline and also pulls recent posts from the handful of large accounts they follow, merging the two lists. Two refinements help further: users inactive for weeks are skipped during fan-out and have their timeline rebuilt on return, and each timeline is capped at a few hundred IDs.

Fan-out on read

  • + Cheap writes
  • − Query everyone you follow
  • − Heavy work on every read

Fan-out on write

  • + Feed read = one lookup
  • − Celebrity = 200M writes
  • − Wasted on inactive users

HybridChosen

  • + Fast reads for everyone
  • + No celebrity write storm
  • − Two code paths
  • Celebrity = more than ~1M followers → pulled and merged at read time
  • Inactive users: skip fan-out, rebuild the feed when they return

Deep Dive 3 — Fast Feed Reads

Each user's Redis timeline is a capped list of roughly the latest 500 post IDs. Most sessions never scroll that far, and anyone who does falls back to querying the database. Post details live in a separate shared cache, so a viral post is stored once and reused by millions of timelines rather than copied into each.

Post IDs are time-sortable, in the style of Snowflake IDs, meaning a larger ID is always a newer post. That makes merging celebrity posts into a timeline a simple merge by ID, and it powers cursor pagination: the next page begins after the last ID the client saw, so no posts repeat or go missing even as new ones arrive. Finally, posts are partitioned in Cassandra by author and clustered by post ID, so fetching an account's latest posts, which the celebrity pull path relies on, is a single partition read.

  • A capped Redis timeline per user

    The latest ~500 post IDs; deeper scrolling falls back to the database.

  • A shared post cache

    A viral post is cached once and reused by millions of feeds.

  • Time-sortable IDs

    Snowflake-style IDs grow with time, so merging feeds is a merge by ID.

  • Cursor pagination

    The next page starts after the last ID seen — no duplicates, no gaps.

  • Posts partitioned by author

    Cassandra partition per user, sorted by post ID: one read for latest posts.

Trade-offs

The hybrid fan-out gives fast reads without write storms, at the price of maintaining two code paths and a threshold that needs tuning. Eventual consistency keeps the system available, though a new post may reach some followers a few seconds after others. Precomputed timelines make reads instant but consume Redis memory for every active user, which is why timelines are capped and inactive users are skipped.

Choosing a chronological feed keeps behaviour simple and predictable for now, and a ranking stage can later be inserted between fetching candidate post IDs and returning the page, without changing the storage or fan-out design. In short: push posts to most followers, pull from celebrities when reading, and let a CDN deliver every byte of media.

  • Hybrid fan-out

    Fast reads and no write storms — but two code paths to maintain.

  • Eventual consistency

    Highly available — new posts reach some feeds a few seconds later.

  • Precomputed feeds

    Instant reads — but extra Redis memory for every active user.

  • Chronological vs ranked

    Simple and predictable today — ranking can be added later as a step.

Push for most, pull for celebrities, CDN for every byte of media.

Key takeaways

  • Feed reads outnumber posts about fifty to one, so do the feed work when a post is written.
  • Upload media directly to S3 with presigned URLs and serve every file from a CDN.
  • Hybrid fan-out: push to followers of normal accounts, pull celebrity posts at read time.
  • Cap Redis timelines at a few hundred post IDs and keep post details in a shared cache.
  • Time-sortable IDs make merging timelines and cursor pagination simple and stable.
  • Skip fan-out for inactive users and rebuild their timeline when they come back.

Interview questions

How does an Instagram-like feed handle celebrities with millions of followers?

Use a hybrid fan-out. Posts from ordinary accounts are pushed into each follower's precomputed timeline at write time. Accounts above a follower threshold, around a million for example, are skipped. When a user opens the feed, the service reads their timeline and pulls recent posts from the few celebrities they follow, merging the results by time-sortable post ID.

What is the difference between fan-out on write and fan-out on read?

Fan-out on write copies each new post ID into every follower's timeline when it is published, making feed reads a single cheap lookup but making posts from huge accounts very expensive. Fan-out on read builds the feed at request time by querying followed accounts, which keeps writes cheap but makes every feed load slow. Large social apps usually combine both.

How should a photo sharing app store and serve images?

Clients upload directly to object storage using presigned URLs, with multipart upload for large files. Background workers create thumbnails and multiple sizes, and transcode videos into several bitrates for adaptive streaming. Because processed files never change, a CDN caches them with long expiry times, so most viewers are served from a nearby edge location and requests rarely reach the origin.

How do you paginate a social media feed without duplicates?

Use cursor-based pagination with time-sortable post IDs. The client sends the ID of the last post it received, and the server returns posts with smaller IDs. Because new posts always get larger IDs, they never shift existing pages, so users see no repeated or skipped posts while scrolling even as fresh content is published at the top.

Why store a news feed as post IDs in Redis instead of full posts?

Storing only IDs keeps each timeline small, so millions of users' feeds fit in memory. Post content lives once in a shared cache and is attached when the feed is read. A viral post therefore occupies memory a single time instead of being duplicated into millions of timelines, and edits or deletions update one cached object rather than many copies.

Keep practising