The Newsfeed Generation Service
In one line: the first sentence of this section contains a design decision the chapter never labels — and it is Lesson 4's hybrid, applied to readers instead of authors.
The branch before step one
When a request from a user (say Alice) to retrieve a newsfeed is received at the web server, the web server either:
- Calls the newsfeed generation service to generate feeds, because some users don't often visit the platform, so their feeds are generated on their request.
- Fetches the pre-generated newsfeed for active users who frequently visit the platform.
This is a second hybrid axis, and the chapter presents it as an implementation detail
Lesson 4 established the hybrid by author: push for small accounts, pull for large ones. This is a hybrid by reader:
ACTIVE user -> feed was precomputed -> cache read (push) LAPSED user -> generate on demand -> full pipeline (pull)
Same trade, different axis, and this one is arguably the larger saving.
The chapter's own assumptions make it concrete: 1 billion registered users, 500 million daily active. So on any given day, half the user base never opens the app — and over a month the ratio is far worse, because "daily active" churns.
Precomputing feeds for users who will not look at them is pure waste. Lesson 3's storage estimate implicitly assumes you do exactly that, computing 200 posts for every one of 500 million users.
So there are two hybrid axes, and a complete design uses both:
| Axis | Push when | Pull when |
|---|---|---|
| By author (Lesson 4) | Few followers | Many followers |
| By reader (here) | Active user | Lapsed user |
Precompute only where the precomputation will be consumed. That is the general form, and it is why the fan-out cost in a real system is far below the naive posts × followers figure.
The honest gap: the chapter gives no criterion for "frequently visits." Real systems use a decay — last-seen recency, sessions per week — and demote users out of the precompute set as they lapse, then rebuild on return.
The six steps
- Retrieve IDs of all users and entities that Alice follows from the graph database.
- Get their information from the user cache, which is regularly updated whenever the users' database is modified.
- Retrieve the latest, most popular, and relevant posts for those IDs from the post cache.
- The ranking service ranks posts based on their relevance to Alice.
- The newsfeed is stored in the newsfeed cache, from which the top N posts are published to Alice's timeline.
- Whenever Alice reaches the end of her timeline, the next top N posts are fetched from the newsfeed cache.
Step 3 is where the candidate set is bounded, and it is the least specified step
"Retrieve the latest, most popular, and relevant posts for those IDs."
Three adjectives doing an enormous amount of work, and this is the step that decides whether the pipeline is affordable.
Lesson 2's arithmetic: 550 connections producing a few posts each is roughly 2,750 candidates per day, of which 200 are shown. But the raw candidate pool is larger still if you do not bound it — a page posting hourly, a friend on a thread, weeks of backlog for a returning user.
So step 3 must apply cheap filters before expensive ranking:
CHEAP: recency window -> per-source cap -> popularity prefilter
|
EXPENSIVE: rank the survivors
That ordering is the whole reason the pipeline is viable. Lesson 10's ranking evaluates "thousands of features" per post — you cannot afford to run it over an unbounded set, so retrieval must produce a bounded candidate list first.
Every ranking system is really a funnel: cheap filters narrow, expensive models order. The design's three adjectives are three filters, and naming them as a funnel is what turns the sentence into a design.
Steps 5 and 6 describe pagination over a frozen list — which the API cannot express
Step 5 stores the ranked feed and publishes the top N. Step 6 fetches the next N when Alice scrolls.
That is infinite scroll over a materialized, ordered list — and it confirms what Lesson 6 argued the API needs and lacks.
The important property is that the list is frozen. Alice's feed was ranked once, at generation time. Scrolling walks through that stored order; it does not re-rank.
Why that matters:
Re-ranking mid-scroll would be incoherent. If position 21 were computed fresh, posts already seen could reappear and unseen ones vanish.
It requires a cursor. getNewsfeed(user_id, count) has none, so there is nothing to express "I have consumed the first 20 of my stored feed."
It bounds how stale a session gets. A user scrolling for ten minutes is reading a ranking computed before they started. New posts arriving during that time are not inserted — they appear on the next refresh.
A ranked feed must be materialized before it can be paginated, and that is one of the strongest arguments for precomputation over pure on-demand generation.
The chapter's own Q&A contains the correction to its storage estimate
The design asks: "The creation and storage of newsfeeds for each user in the cache requires an enormous amount of memory. Is there any way to reduce this?"
And answers:
A more memory-efficient approach stores an index that maps each user_id to a list of recent post_ids. When a feed request arrives, the system retrieves the list of followed users, fetches their recent post_ids from the index, loads the corresponding posts from storage, and then merges and ranks the results. This avoids precomputing and storing complete feeds for every user.
That is exactly right, and it is the correction Lesson 3's estimate needed — the estimate computed 56 PB by storing complete feeds with media for every user, while this Q&A says store post IDs.
Two observations.
It is the same rule as every previous chapter. Twitter's timelines stored IDs; Uber's design separated latest-position from route traces; here, feeds are indexes. Fan out references, not values.
But read carefully — the answer describes something closer to pull. "Fetches their recent post_ids from the index, loads the posts, merges and ranks" is on-demand assembly, not a precomputed feed. So the memory saving comes partly from storing IDs and partly from not precomputing at all.
Those are two different optimizations bundled together, and it is worth separating them:
Store IDs instead of content -> ~99% memory reduction, KEEPS precomputation Don't precompute at all -> 100% reduction, but pays cost on every read
The first is unambiguously right. The second is Lesson 4's push-versus-pull trade, and the right answer is the hybrid rather than abandoning precomputation.
Step 2's cache-coherence claim deserves a caveat
"Get their information from the user cache, which is regularly updated whenever the users' database gets updated/modified."
"Regularly updated whenever" is doing two incompatible things — regularly suggests periodic refresh, whenever suggests invalidation on write.
They are different strategies with different failure modes:
| Write-through / invalidate | Periodic refresh | |
|---|---|---|
| Staleness | Near zero | Up to the refresh interval |
| Cost | A cache operation per DB write | Constant background load |
| Fails by | Missed invalidations | Predictable lag |
For user metadata — display names, avatars — periodic refresh is fine, since Lesson 2 already chose availability over consistency. Nobody is harmed by a day-old avatar.
Worth naming only because "the cache is updated whenever the database is" is the kind of claim that hides a real design decision. Cache coherence strategy is a choice, and the acceptable staleness should be stated per data type.
Viral posts break an ordinary cache
Feed generation reads posts constantly, so a cache in front of the post store is obvious. What is less obvious is that the standard sharded cache reproduces the exact problem it was meant to solve.
Sharding a cache by post id distributes keys evenly, which is the right instinct when load per key is roughly uniform. Feed traffic is not uniform: a single post from a large account can take a meaningful fraction of all reads, and every one of those reads lands on the one node that owns that key.
Replication inverts the trade. Instead of each post living on exactly one node, every cache instance can serve any post, and a load balancer spreads requests across them. A viral post's traffic is divided by the number of instances rather than concentrated. The instances never coordinate, because they are all serving the same immutable thing.
Two properties make this cheap here. Posts are written once and almost never edited, so replicas cannot disagree in any way a reader would notice — and on the rare edit you invalidate that post id everywhere. And the working set is small and recent: most posts are read heavily for a day or two and then effectively never, so an LRU policy with a long TTL keeps exactly the right things resident.
The cost is memory — you hold the hot set N times rather than once — and that is the trade you are making deliberately: spend memory to flatten a load spike you cannot predict.
Key takeaway
The branch before step one is a second hybrid axis — precompute for active users, generate on demand for lapsed ones — and with half the registered base inactive daily, it is the larger saving. Precompute only where the precomputation will be consumed. Step 3 bounds the candidate set and is the least specified step: every ranking system is a funnel, with cheap filters narrowing before expensive models order. Steps 5 and 6 describe pagination over a frozen, materialized list, which the API cannot express and which is why a ranked feed must be materialized before it can be paginated. And the chapter's own Q&A gives the storage correction — store post IDs, not feeds — though it bundles that with abandoning precomputation, which are two separate decisions.
Next: publishing the generated feed.