Guide

A news feed shows a user recent posts from the people they follow, optionally ranked. It looks like a simple CRUD app until you multiply by hundreds of millions of users. Then the hard problem stops being storage and becomes fan-out: getting each new post to the right readers fast enough that opening the app feels instant.

Before any design, pin down the requirements and the numbers. The numbers decide the rest.

Requirements

Functional

  • Create a post with text and optional media.
  • Follow and unfollow another user.
  • Load a home timeline of posts from followees (the accounts a user follows), newest first or ranked.
  • Optional: like and repost counts on each card, which can be eventually correct.

Non-functional

  • About 300M daily active users (DAU).
  • A home feed p99 under 200 ms for the first page. p99 is the response time that 99% of requests beat.
  • A new post appears in followers' feeds within a few seconds. Eventual consistency is fine.
  • High availability on reads. A brief fan-out lag is better than a feed outage.

Out of scope unless asked: full-text search, direct messages, ad auctions, stories.

Estimates

DAU:                300M
Posts per user/day: ~0.2  -> 60M posts/day  = ~700 posts/s on average (~2k at peak)
Feed loads per user: ~20/day -> 6B loads/day = ~70k loads/s on average (~200k at peak)
Average followees:  ~200
Average followers:  ~200 (heavy-tailed: a few accounts have 100M)

Storage for post metadata is modest. At 1 KB per post, 60M posts a day is about 60 GB a day, or 22 TB a year. Media goes to object storage and a CDN, and it dominates the storage bill.

The number that matters most is the write load for fan-out. The fan-out page works through it.

API

HTTP
POST /v1/posts
{ "text": "...", "mediaIds": ["m_1"] }
-> 201 { "postId": "p_987", "createdAt": "..." }

POST /v1/users/{id}/follow    -> 204
DELETE /v1/users/{id}/follow  -> 204

GET /v1/feed?cursor=eyJ0cyI6Li4ufQ&limit=20
-> 200 { "items": [{ "postId", "authorId", "text", "media", "stats" }], "nextCursor": "..." }

Use cursor pagination with a timestamp and post ID, not offsets. An offset skips or repeats posts when new ones arrive mid-scroll, and it gets slower the deeper you page.

Data model

EntityKey and accessStoreNotes
Useruser_idPostgres or KVProfile and flags such as is_celebrity
Postpost_idSharded KV or wide-columnSource of truth for content
Follow(follower_id, followee_id)Graph table, TAO-likeNeeds both directions: who I follow, and my followers
Timelineuser_id to sorted post refsRedis sorted setDerived, can be rebuilt
MediaObject keyS3 and CDNUploaded with a presigned URL

Posts are immutable content, and edits become new versions. Timelines store references (post_id and a timestamp), not full bodies. The feed hydrates the bodies from a post cache on read.

Common mistakes

  • Estimating only the average. Peak here is about three times the average. Design for peak, then handle the tail as a special case.
  • Using offset pagination. Posts get skipped or duplicated while users scroll.
  • Storing media in the database. Keep media in object storage and serve it through a CDN.

Check yourself

  1. Using the estimates, how many posts per second does the system write on average?

    About 700: 60 million posts a day, divided by 86,400 seconds.

  2. Why does the API use a cursor instead of an offset?

    A cursor marks a specific position by timestamp and post ID, so new posts arriving mid-scroll don't shift the pages.

Key takeaways

  • The numbers (DAU, posts per second, feed loads per second) shape the architecture.
  • Fan-out cost, not storage, is the hard part of the design.
  • Timelines hold references, and posts remain the source of truth.
  • Use cursor pagination, and keep media off the database.

Further learning

Facebook's TAO paper describes the geographically distributed social graph store that sits under the follow data model.

Facebook TAO: The Power of the Graphatc13-bronson.pdfusenix.org

Next: fan-out on write and read.