Open any social app and the first thing you see is a feed: a list of recent posts from the people you follow. It looks like the simplest screen in the app. It is also one of the most asked system design questions, because one small idea decides everything: do you build the feed when someone posts, or when someone reads?
Most of this note is about that one question. Everything else (storage, ranking, counters, photos) hangs off it.
1. Problem Statement
Design the feed for a social network like Instagram, X or Facebook. Users follow other users. They publish posts with text and maybe a photo. When a user opens the app, they see a page of posts from the accounts they follow, newest and most interesting first. They can scroll down to load more, like a post, and comment on it.
What makes it hard is the shape of the data. Most people have a few hundred followers. A few accounts have tens of millions. The same design has to work for both, and a design that is perfect for one is a disaster for the other.
2. Questions to Ask First
| Question | The assumption I will make |
|---|---|
| How many users? | 500 million accounts, 300 million active in a week, 200 million active every day |
| How do people follow? | One way, like Instagram or X. You can follow someone without them following you back. |
| How many followers does a person have? | Typical user has 300. A few thousand accounts have more than 1 million. The top ones have 30 million. |
| What does the feed show? | Posts only from accounts you follow. No ads and no suggestions from strangers. |
| What order? | Recent first, then a simple score on top. We will build the simple one first. |
| How fresh? | A post from a normal account should show up in followers' feeds within about 5 seconds. A celebrity post within 30 seconds is fine. |
| Can a feed be slightly out of date? | Yes. Eventual consistency is fine. Missing a post for a few seconds hurts nobody. |
3. Requirements
3.1 Functional Requirements
| Requirement | Detail |
|---|---|
| Publish a post | Text plus up to 4 photos |
| Follow and unfollow | Changes whose posts appear in my feed |
| Read my feed | A page of 20 posts, with more on scroll |
| See a profile | One person's posts, newest first |
| Like and unlike | One like per user per post, and a visible like count |
| Comment | Add and read comments, with a visible comment count |
| Delete a post | The post disappears from every feed |
3.2 Non Functional Requirements
- Reading the feed must be fast. Under 300 ms at p99, because it is the first screen of the app.
- Reads far outnumber writes. People look at feeds all day and post once in a while. The design should push work to the write side to keep reads cheap.
- Highly available. A broken feed means the app looks broken. Better an older feed than an error.
- Eventually consistent. Two people may see slightly different states for a few seconds.
- Handles the celebrity case. A post from someone with 30 million followers must not hurt anyone else.
- Scrolling must be stable. Posts must not repeat or jump while a user scrolls down.
4. Back of the Envelope Numbers
Reads first.
daily active users = 200 million
feed opens per user = 10 per day
feed opens per day = 200M x 10 = 2 billion
per second on average = 2,000,000,000 / 86,400 = about 23,000
at peak (3 times average) = about 70,000 feed reads per secondWrites.
people who post each day = 10 percent of 200M = 20 million posts per day
per second on average = 20,000,000 / 86,400 = about 230
at peak = about 700 posts per secondSo reads outnumber posts by 100 to 1 (70,000 against 700). That ratio is what we should design around.
Now the two ways to build a feed, with real numbers. This is the part to do slowly.
Fan out on read. When you open the app, we look up the 300 accounts you follow, fetch each one's recent posts, and merge them.
lookups per feed read = 300 accounts
at peak = 70,000 x 300 = 21 million lookups per secondFan out on write. When someone posts, we push the post id into the feed of each follower.
posts per day = 20 million
followers per post = 300 on average
if all followers get it = 20M x 300 = 6 billion inserts per day
only active followers = 60 percent of them = 3.6 billion per day
per second on average = 3,600,000,000 / 86,400 = about 42,000
at peak = about 125,000 inserts per second21 million lookups per second against 125,000 inserts per second. Fan out on write is about 170 times cheaper at peak, because we pay once per post instead of once per read, and reads are 100 times more common.
Now the celebrity problem.
followers = 30 million
one post = 30 million inserts
at 100,000 inserts per sec = 30,000,000 / 100,000 = 300 seconds = 5 minutesThe last follower sees the post 5 minutes late, and for those 5 minutes that one post uses up the fan out workers. If 100 such accounts post 3 times a day:
100 accounts x 3 posts x 30M followers = 9 billion inserts per dayThat is more than the 3.6 billion from all normal users put together. A hundred accounts would more than double our write load. This is why one simple rule cannot cover the whole network.
Feed storage. Each feed holds the latest 300 post ids. Nobody scrolls further.
one entry in a Redis sorted set = about 48 bytes (an 8 byte id plus overhead)
one feed = 300 x 48 B = 14.4 KB
feeds for 300M weekly users = 300M x 14.4 KB = 4.3 TB
with one replica = 8.6 TB, about 215 nodes at 40 GB eachPost storage is small. 20M posts x 1 KB = 20 GB a day, 7.3 TB a year. One database can hold that for years.
Photos are the big one.
posts with a photo = 30 percent of 20M = 6 million per day
size of all versions = 1.5 MB (original plus 3 smaller sizes)
storage = 6M x 1.5 MB = 9 TB per day, 3.3 PB per year
delivery at peak = 70,000 pages x 6 photos x 150 KB = 63 GB per second = about 500 Gbit per secondNo set of our own servers should serve 500 Gbit per second. That number is the whole case for a CDN, a network of servers around the world that keeps copies of the photos close to the viewer.
Likes: 5 per user per day is 1 billion a day, about 11,600 per second, about 35,000 at peak. Fine for the system as a whole. One viral post is the trouble, and section 9.3 deals with it.
5. High Level Design
5.1 Walk Through of Each Box
API gateway. The front door. It checks the login token, limits how fast one user can call us, and sends the request to the right service.
Post service. Takes a new post, stamps it with an id, saves it, and drops a "post created" message onto a queue. It does not do the fan out itself. If it did, the user would wait while we copy the post to 300 feeds. Posting should return in about 100 ms, so the heavy part happens after.
Message queue. A line of work waiting to be done. The post service puts a message in, and workers take it out at their own speed. If a burst of posts comes in, the queue gets longer for a moment and nothing breaks. Think of the ticket line at a post office: the clerk is not slowed down because ten people arrived at once, the line just grows.
Fan out workers. They read a "post created" message, ask the follow graph who follows the author, and push the post id into each follower's feed. "Fan out" just means one thing spreading out to many places. Workers can be added or removed freely, since each message is independent.
Follow graph service. Answers two questions: who does this person follow, and who follows this person. Two different questions, so two different indexes (section 7).
Feed store. One list per user, holding the ids of the latest 300 posts that should appear in their feed. It is in memory, because it is read 70,000 times a second. It holds ids, not full posts. A post id is 8 bytes, a post is 1 KB, so the feed stays small and we never copy a post's text 300 times.
Celebrity recent posts list. A small list per celebrity of their last 20 or so posts. Used only for the pull half of the hybrid, see section 9.1.
Post store. The real posts. Sharded by post id so the load spreads evenly. Also cached in front, since a hot post is read by millions.
Feed service. The read side. It gets the id list, merges in celebrity posts, loads the full posts, ranks them, and returns a page.
Counter service. Likes and comment counts, kept apart from the posts because they change so fast (section 9.3).
Media service and CDN. Photos never pass through our app servers. The app uploads directly to object storage, and the CDN serves them (section 8.1).
6. API Design
All calls need a login token. {id} values are 64 bit numbers sent as strings, so JavaScript does not round them.
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/media/uploads | Ask for a place to upload one photo |
| POST | /v1/posts | Publish a post |
| GET | /v1/feed | My feed, one page |
| GET | /v1/users/{id}/posts | A profile's posts |
| DELETE | /v1/posts/{id} | Delete my post |
| POST | /v1/users/{id}/follow | Follow |
| DELETE | /v1/users/{id}/follow | Unfollow |
| PUT | /v1/posts/{id}/like | Like (safe to repeat) |
| DELETE | /v1/posts/{id}/like | Unlike |
| POST | /v1/posts/{id}/comments | Add a comment |
| GET | /v1/posts/{id}/comments | Comments, one page |
Like is PUT and not POST on purpose. Liking twice must give the same result as liking once, because phones retry
requests on bad networks. PUT says that out loud.
6.1 Upload a Photo
Send { "contentType": "image/jpeg", "sizeBytes": 2480000 } and get back a mediaId, an uploadUrl and
expiresInSeconds: 600. The uploadUrl is a signed link. It lets the phone send the file straight to object storage
for 10 minutes, and nothing else. Our servers never touch the bytes.
6.2 Publish a Post
Send a header Idempotency-Key: 7c1e... so a retry does not create two posts, and a body like this:
{ "text": "Sunrise from the train", "mediaIds": ["902110553"] }Response is 201 Created with the new postId, authorId and createdAt.
6.3 Read the Feed
GET /v1/feed?limit=20&cursor=eyJzIjoiYWJjMTIzIiwibyI6MjB9Response:
{
"items": [
{
"postId": "1790234001122",
"author": { "id": "4411", "name": "Asha" },
"text": "Sunrise from the train",
"media": [{ "url": "https://cdn.example.com/m/902110553_640.jpg" }],
"likeCount": 128,
"commentCount": 9,
"likedByMe": true,
"createdAt": "2026-10-10T06:12:44Z"
}
],
"nextCursor": "eyJzIjoiYWJjMTIzIiwibyI6NDB9"
}The cursor is a blob that only the server understands. The app hands it back untouched. When nextCursor is null, there
is nothing more.
7. Data Model
Several stores, each picked for how it is read.
7.1 Post Ids
Posts get ids in the style of Snowflake: a 64 bit number made of 41 bits of time in milliseconds, 10 bits for the machine, and 12 bits for a counter within that millisecond.
Why not a plain auto number? One database would have to hand them out, and that is a bottleneck. Why not random? Then sorting by id tells us nothing. With a time-first id, sorting by id is sorting by time. That means "newest first" needs no separate time column in the index, and a cursor can be just "an id".
7.2 Tables
users
| Column | Type | Why |
|---|---|---|
user_id | bigint | Primary key |
handle | text | Unique name. A unique index, because people look it up. |
follower_count | bigint | Kept here so we can decide in one read whether the account is a celebrity |
is_celebrity | boolean | Set when followers pass 100,000 (see section 9.1) |
following and followers
| Table | Partition key | Clustering key | Why |
|---|---|---|---|
following | user_id (me) | followee_id | All of my follows sit together. "Do I follow X" is a direct lookup. |
followers | user_id (the account followed) | follower_id | Fan out workers read my followers in pages of 1,000 |
Both tables hold the same fact, "A follows B", in two directions, and the write is done twice. It looks wasteful, but each question ("who do I follow", "who follows me") is then a single partition read. With one table, one of the two would have to scan.
posts
| Column | Type | Why |
|---|---|---|
post_id | bigint | Primary key and shard key. Random-looking spread across shards (hash of id), so no shard is hotter. |
author_id | bigint | Who wrote it |
text | text | Up to 500 characters |
media_ids | list of bigint | Up to 4 |
created_at | timestamp | |
is_deleted | boolean | Soft delete. The row is kept for 30 days, so a delete can be undone. |
author_posts (a profile's timeline)
| Column | Type | Why |
|---|---|---|
author_id | bigint | Partition key. A profile page asks only about one author. |
post_id | bigint | Clustering key, sorted newest first. Reading 20 posts is one short scan. |
This table is also what the pull half of the hybrid reads. A celebrity's last posts are the top of their partition.
post_likes is partitioned by post_id with user_id as the clustering key. The pair is unique, so a double like is
just an overwrite.
post_counters has post_id as primary key and two numbers, like_count and comment_count. It is a saved copy,
refreshed in batches (section 9.3). comments is partitioned by post_id with a time-ordered comment_id as the
clustering key, so reading the first page of comments is one short scan.
media
| Column | Type | Why |
|---|---|---|
media_id | bigint | Primary key |
owner_id | bigint | Who uploaded it |
object_key | text | Where the original sits in object storage |
status | enum | UPLOADING, PROCESSING, READY, FAILED. A post only accepts READY media. |
variants | json | Sizes made, such as 320, 640, 1080 |
7.3 The Feed Itself
Not a table. It lives in the in-memory feed store, as one sorted list per user.
key feed:{user_id}
member post_id
score post_id (time-first, so the id is already a time)
size newest 300 onlyAdding to a feed is one command: add the id, then cut the list back to 300. Reading a page is one command: give me the 20 ids just below this cursor. Both are fast because the list is already sorted.
Why score by post id and not a separate timestamp? One less number to store, and two posts in the same millisecond still have a clear order.
Why 300? On an ordinary day nobody goes more than 100 posts deep. 300 is three times that. If someone does reach the end, we fall back to the pull path, which can go as deep as we want, just slower.
8. Key Flows
8.1 Publish a Post with a Photo
The user gets their 201 before any fan out happens. Their own feed is handled by adding the post to it right away in
the post service, so they always see what they just posted, even if the workers are a few seconds behind.
The photo takes a different road from the text. It goes phone to storage directly, never through our servers. A photo is a few MB and a post is 1 KB, so if photos came through the app servers, 99 percent of their bandwidth would be spent carrying photos. The resizing step makes 3 smaller copies, so a phone on a slow network can ask for the 320 pixel one.
8.2 Read the Feed (Hybrid)
Count the lookups: one for the feed list, one for the follow check, one for the celebrity lists (batched), one for the post bodies (batched), a few counters. About 5 round trips, in parallel where they can be. Fan out on read alone needed 300.
8.3 Like a Post
The insert is the guard. Only a real first like changes the counter, so a retried request cannot add 2.
9. Deep Dives
9.1 Fan Out on Write, Fan Out on Read, and the Hybrid
The problem. Every follower must be able to see a new post, and the feed must open in a split second. Where does the work go, at post time or at read time?
Option one: fan out on write (push). When a post is made, copy its id into the feed of every follower.
- Reading is trivial: one list, already sorted. About 5 ms.
- Writes cost one insert per follower. For 300 followers that is 300 inserts (about 180 once we skip inactive people), around 40 ms of worker time at 5,000 inserts per second per worker. Perfectly fine.
- Wasted work for people who never open the app. We skip those who have not been seen in 2 weeks, which saves about 40 percent.
- For 30 million followers, one post is 30 million inserts and 5 minutes of delay, as worked out in section 4.
Option two: fan out on read (pull). Store nothing extra. When someone opens the app, fetch the recent posts of each account they follow and merge them.
- Posting is trivial: one row.
- Reading is heavy: 300 lookups per open, 21 million lookups per second at peak.
- No wasted work. Nobody pays for feeds that are never opened.
- A celebrity's post is read by everyone, but it is one hot, cached list, which is exactly what caches are good at.
Neither wins alone. Push is great for the 300 follower majority and terrible for the 30 million follower few. Pull is terrible for the many reads of ordinary accounts and fine for the few celebrities, because celebrity posts are a small set that can be cached.
The choice: do both, split by follower count. This is the hybrid.
- An account with fewer than 100,000 followers is pushed. Fan out workers copy its posts into follower feeds.
- An account with more than 100,000 followers is pulled. Its post is saved once and added to its own small recent posts list. Nothing is pushed.
- When someone reads their feed, the feed service takes the pushed list, finds which celebrities this user follows, reads their recent lists, and merges the two.
Why 100,000? It is where push starts to cost more than it saves. At 100,000 followers one post is 100,000 inserts, about 1 second at 100,000 per second. That is still acceptable. At 1 million it is 10 seconds, and at 30 million it is 5 minutes. Anything between 100,000 and 1 million works. I would start at 100,000 and move it by watching how long the slowest fan out takes.
Check that the read side stays cheap. A user follows 300 accounts and, say, 4 of those are above the line. The read is 1 feed list plus 4 celebrity lists, merged. The celebrity lists are the hottest data in the system, read tens of thousands of times per second at peak for the biggest ones, so they live in memory and get copied locally in the feed servers for a second or two. Reading them is nearly free.
Only a few thousand accounts out of 500 million are over the line, roughly 1 in 100,000. We pay a heavier read for those few and save all of their fan out cost.
Two details people get wrong.
First, an account that crosses the line. Say an account reaches 100,000 followers. From then on its posts are pulled. The old ones already pushed stay in feeds, which is harmless. To stop it flipping back and forth when the follower count hovers around 100,000, make it a celebrity at 100,000 and only turn it back at 80,000. That gap is called hysteresis, and it stops the account flipping between modes every few minutes.
Second, a new follow. If you follow someone new, your feed list has none of their old posts. On follow, copy that author's last 10 posts into the follower's feed. If the author is a celebrity, there is nothing to copy, the pull path covers it. On unfollow, do not hunt through the feed. Filter at read time: when merging, drop any post whose author you no longer follow. That check is cheap since we already loaded the follow list.
Deletes work the same way. We do not remove a deleted post from 300 feeds. We mark it deleted, and the feed service drops it when it loads post bodies, because a deleted post comes back as "gone". The feed list gets cleaned up by itself as new posts push the old id off the end of the 300.
9.2 Ranking and Pagination
The problem. A raw time ordered feed is fine, but people follow a lot of accounts and miss the posts they care about. And once we have an order, we must page through it without repeating or skipping posts while new ones keep arriving.
Ranking first.
Option one: pure recency. Sort by post id, newest first. It needs no extra data, it is easy to explain to users ("this is the order things happened"), and it is predictable. Its weakness is that a chatty account crowds out a quiet one you care more about.
Option two: recency plus a score. Take the candidate posts and give each a number, then sort by it.
score = affinity x (1 + log10(1 + likes + 2 x comments)) / (1 + age_in_hours / 3)Read it in three parts.
- Affinity is how much you care about this author, from 1 to 5. We work it out once a night from how often you like, comment on or open that person's posts in the last 30 days. It is one number per pair, stored in a small table.
- The log part rewards posts that others like, but a post with 50,000 likes is not 1,000 times better than one with 50. The log squeezes the big numbers down.
- The last part is the age penalty. A post loses about half its score after 3 hours, and a bit more every hour after.
Try it with three posts.
A: close friend (affinity 3), 2 hours old, 10 likes, 2 comments
engagement = 10 + 4 = 14, log10(15) = 1.18, so 2.18
age part = 1 + 2/3 = 1.67
score = 3 x 2.18 / 1.67 = 3.9
B: a stranger you follow (affinity 1), 30 minutes old, no likes
score = 1 x 1 / 1.17 = 0.86
C: celebrity (affinity 1), 10 hours old, 50,000 likes, 2,000 comments
engagement = 54,000, log10 = 4.73, so 5.73
age part = 1 + 10/3 = 4.33
score = 1 x 5.73 / 4.33 = 1.32Order is A, then C, then B. The close friend's post from two hours ago beats a brand new post from a stranger, and the famous post still shows above the newest post from a stranger. That feels about right, which is how you tune these things: try real examples and see if the order makes sense.
The choice: ship pure recency first, then add the score. I would not start with the score. You need data on likes and affinity before the numbers mean anything, and recency is a good feed on its own. When you add the score, only rank the top 200 candidates, the 300 pushed ids plus the pulled celebrity posts, cut down to the newest 200. The score is a few multiplications, so 200 of them take well under a millisecond, and we do not need to score the entire network.
Pagination.
Option one: offset. GET /feed?page=3, which means skip 40 and give 20. It is the first thing everyone thinks of, and
it breaks. Say you load page 1 (posts 1 to 20). While you read, 5 new posts arrive. You ask for page 2, which is "skip
20", but everything has moved down by 5. Now page 2 starts with 5 posts you already saw.
Option two: a cursor. The server says "the last thing I gave you was this one", and the next request means "give me what comes after that". New posts at the top do not matter, because the position is tied to a post and not to a number.
For a time ordered feed, the cursor is just the id of the last post you saw. The query is "20 ids with a score below this id", which is a direct jump in a sorted list.
For a ranked feed, there is a catch. Scores change as likes arrive. If page 2 re-ranks from scratch, a post you saw on page 1 might slide down to page 2, or one you skipped might jump up. The fix is a snapshot:
- On the first request, rank the top 200 candidates and save the ordered ids as a list for 15 minutes, under a session id.
- The cursor holds the session id and the position, for example "session abc123, offset 20".
- Page 2 reads positions 20 to 39 of the saved list. No re-ranking, so no repeats and no jumps.
- Pull to refresh starts a new session.
What does it cost? A session is 200 ids x 8 B = 1.6 KB. With 23,000 feed opens per second and a 15 minute life, that is 23,000 x 900 = 20 million live sessions, about 33 GB. Small compared with the 8.6 TB of feeds. Worth it for a stable scroll.
9.3 Likes and Comment Counters
The problem. Every post shows a like count. Likes are the highest volume write in the system after the feed fan out, at 35,000 per second at peak. And they are not spread evenly. One viral post may get 50,000 likes in a single second.
Option one: update a number in the database. UPDATE post_counters SET like_count = like_count + 1. Every like locks
that row for a moment. A single database row handles about 1,000 updates per second at best. A hot post at 50,000 per
second would queue up 50 times what the row can take, and every other query waits behind it.
Option two: count in memory and save in batches. Add to a counter in the in-memory store (one command, 100,000 a second per node is normal), and let a background job write the totals to the table every 5 seconds.
Choose option two. 50,000 likes per second become 1 database write per post every 5 seconds. A viral post costs the database almost nothing.
Details to get right.
- The truth is the
post_likestable, where each (post, user) pair is a row. The counter is a convenience copy. If the counter is lost, we can recount from the table. A user liking twice cannot double count because of the unique pair. - A hot counter on one memory node can itself be too hot. Split it into 16 pieces:
likes:{post}:0to:15. A like adds to a random piece, and reading means adding up the 16. This is the same trick as splitting a hot cache key. Only do it for posts that cross, say, 1,000 likes per second. - The count a user sees can be a few seconds behind. Nobody can tell a difference between 1,204,331 and 1,204,380 likes.
- Show the user their own like at once. The response to the like includes
likedByMe: true, and the app adds 1 on the screen on its own, so it feels instant. - For huge numbers, show "1.2M" instead of the exact digits. Exact numbers that change every second look wrong anyway.
Comment counts are the same idea, with less volume (about 1,150 comments a second), so a plain counter in memory with a batch flush is plenty.
10. Failure Handling
| What breaks | What the user sees | What we do |
|---|---|---|
| Fan out workers fall behind | New posts show up late in feeds, maybe by minutes | The queue grows but nothing is lost. Alert on queue age, add workers. The author still sees their own post immediately. |
| A worker crashes mid fan out | Some followers miss the post for a while | The message is not removed from the queue until the work is done, so another worker retries. Adding the same id twice is harmless in a sorted list. |
| A feed store node dies | Those users get an empty or old feed | Replica takes over in seconds. If a feed is gone, rebuild it with the pull path (read recent posts of everyone followed) and refill the list. Slower, but it works. |
| Feed store is down entirely | Feed is slow, older than normal | Fall back to pull for everyone, with a cap on how many follows we read (say the newest 100). Better a partial feed than an error. |
| Counter cache lost | Like counts reset to older numbers | Recount from post_likes and the last saved post_counters. Show the saved number meanwhile. |
| Photo upload fails halfway | Post button stays disabled | The signed link allows retries. Status stays UPLOADING, and a cleanup job removes files older than a day with no post. |
| CDN has a problem | Photos load slowly | Fall back to a second CDN or go straight to object storage for a short time. Text still shows. |
| Celebrity list cache is missing | Celebrity posts absent from feeds | Rebuild from author_posts, one short read per celebrity. Cheap because the table is already sorted by newest. |
| Deleted post appears | A deleted post is seen for a moment | Mark deleted first, then filter at read time. The cache for that post is cleared at the same moment. |
What goes on under all of this: the feed is allowed to be old, but never allowed to be an error. If anything is slow, serve less (a shorter list, a cached page) instead of failing.
11. Tradeoffs
| Decision | Chosen | Alternative | Reason |
|---|---|---|---|
| Feed building | Hybrid, push for most, pull for celebrities | Push for all | A 30 million follower post would take 5 minutes and use up the fan out capacity |
| Feed building | Hybrid | Pull for all | 21 million lookups per second at peak is far too heavy |
| Inactive users | No feed pushed after 14 days | Push to all | Saves about 40 percent of writes. Their feed is rebuilt on return. |
| Ordering | Recency first, then a score on the top 200 | Full machine learning ranking at the start | Needs likes and affinity data first, and recency alone is a good feed |
| Pagination | Cursor, plus a saved ranked session | Offset | Offset repeats posts when new ones arrive |
| Like counts | In memory, flushed every 5 seconds | Update the row | A viral post would overload one row |
12. Interview Talking Points
Say these in about this order.
- Start by saying the whole problem is one choice, build the feed at write time or read time, and that you will come back to it with numbers. Then fix the scope in two minutes: follow one way, posts only, 200 million daily users, 100 reads for every post.
- Do the arithmetic for both extremes. Pull is 70,000 reads x 300 follows = 21 million lookups per second. Push is about 125,000 inserts per second at peak. Push wins about 170 to 1 for normal accounts.
- Then show where push breaks: 30 million followers is 30 million inserts and 5 minutes of delay, and 100 such accounts double the daily write load. Only then say "so I will use a hybrid", and give the rule: push below 100,000 followers, pull above.
- Say what the read looks like in the hybrid: one list from the feed store plus about 4 celebrity lists, merged, so about 5 lookups instead of 300. Mention that the feed holds ids, 8 bytes each, and not whole posts.
- Mention the small ugly cases without being asked: new follow copies 10 old posts, unfollow and delete are filtered at read time, the celebrity line has a gap to stop flipping. This is what shows you have thought past the diagram.
- Ranking: say you would ship recency first, and describe the score in one line (affinity, likes on a log scale, age penalty). Say you only rank the top 200.
- Pagination: explain why offset repeats posts, use a cursor, and for the ranked feed save a session for 15 minutes so the order cannot change while someone scrolls.
- Finish with the sidebars in one sentence each: likes counted in memory and flushed every 5 seconds because a viral post would melt one row, photos go straight to object storage and out through a CDN because 500 Gbit per second cannot come from our servers, and the feed may be old but must never be an error.