Materialised timelines, denormalization, trimming and invalidation
coreadvancedA materialised timeline is a per-user ordered list of post references, and the first storage decision is how much of each post to copy into it. Storing only the post id keeps the timeline tiny and correct — an edited post is edited once, everywhere — at the cost of a second lookup per item on read. Storing a denormalised snapshot (author name, avatar, first line of text) makes the read a single operation and creates a consistency problem: every copy has to be updated when the source changes, and a display-name change would otherwise touch millions of rows. The usual answer is to denormalise only fields that are immutable for the life of the post and to look up everything mutable, which keeps invalidation work proportional to what actually changes. The second decision is that a timeline is bounded: nobody scrolls to the beginning of time, so timelines are trimmed to a few hundred or thousand entries and deeper history is served by falling back to a query over the source. That single choice turns storage from unbounded — followers times posts, forever — into a fixed cost per user. Hot users need isolation on both sides: a hot author is handled by not fanning them out at all, and a hot timeline is handled by keeping it in a cache with its own replica so one popular account's reads cannot saturate the shard it happens to live on. Finally, invalidation covers four events with different costs: a post edit invalidates cached renderings but not timeline membership, a post delete requires removal or filtering at read time, an unfollow requires removing that author's entries from a timeline, and a block requires both directions — and because every one of these is proportional to audience size, they run asynchronously, exactly like fan-out.
Think of it as
A timeline is a printed contact sheet, not a filing cabinet. It holds small references in an order, it is deliberately short, and it is regenerable from the negatives — which are the posts and follows tables. Once you accept it is a printout rather than the record, the awkward questions get simple answers: an edit changes the negative, so the printout can carry only what never changes; the printout has a fixed number of frames, so old ones fall off the end; and if the printout is lost or wrong you print another one.
What we're doing: Follow a display-name change and a post deletion through two designs, one over-denormalised and one not.
- 8
- Seven hundred million writes for a display-name change is the whole argument against denormalising mutable fields. The failure is not that it is slow — it is that nobody will run it, so the system quietly ships stale data forever.
- 18
- The lookup that replaces it is not free, but its cost is shared: one author record resolves for every entry by that author on the page, and it caches extremely well because author records change rarely.
- 27
- Read-time filtering of unresolvable ids is required regardless of how good the sweep is, because there is always a window between reading the timeline and fetching the posts. Once that filter exists, the asynchronous sweep is an optimisation rather than a correctness requirement.
Why this works: The two events split cleanly: a delete changes membership and must be swept, an edit does not and should never have to be. Keeping mutable fields out of timeline entries is what collapses the second case to nothing, and it is why "denormalise only what is immutable" is the rule rather than "denormalise for speed".
Letting materialised timelines grow without bound
Wrong
Better
What you see: Timeline storage grows faster than the user base and eventually dominates the storage bill, while the tail of that data is read almost never — the overwhelming majority of sessions read the first two pages.
Why: A materialised timeline is a cache of the most recent slice, and a cache without an eviction policy is just a second copy of the database. Bounding it turns per-user storage into a constant and moves the rare deep-scroll case onto a query path that already exists, because that path is also what rebuilds a timeline after a fan-out failure.
- System of record — authoritative, never derived
- posts — full content, mutable
- follows — the graph
- blocks — suppression rules
- Materialised timelines — derived, bounded, rebuildable
- post id + score + author id — immutable fields only
- trimmed to ~1,000 entries — deeper history falls back to a query
- Read caches — shortest-lived, cheapest to lose
- rendered pages — invalidated on edit
- hot timelines — replicated so one reader cannot saturate a shard
What to store in a timeline entry
Four invalidation events and what each costs
Remember: A materialised timeline is a bounded, derived list of references — post id, sort score, author id — and nothing mutable, because a denormalised display name turns a profile edit into hundreds of millions of writes nobody will ever run. Trim it to a fixed length and serve deep history from a query, so per-user storage is a constant. Isolate hot users on both sides: keep hot authors out of fan-out, and serve hot timelines from replicas. And treat invalidation as four distinct events — edit, delete, unfollow, block — each proportional to audience size, so each runs asynchronously with a read-time filter covering the window.
See also: fan out on write vs fan out on read · caching ranking pagination and materialization · stampede hot keys and memory pressure · ttl eviction and invalidation · hot warm cold and retention policies · choosing shard keys

