Timeline Storage and Stories
In one line: the overflow rule for oversized timelines is a small, practical pattern worth knowing, and Stories is where the chapter's own durability requirement gets an exception.
Timeline storage
We store the user's timeline in a key-value store. The key is the userID, and the value is the timeline content (a list of post links). If the value size exceeds the store's limit (e.g. a few MBs), we can store the actual timeline data in a blob store and keep a reference link in the key-value store.
A list of links, not a list of posts — the reference model again
"The value is the timeline content (a list of post links)."
Every chapter in this module has landed on the same rule from a different direction, and here it is stated plainly. The timeline holds links, not posts, and certainly not media.
Why it matters, collected:
Size. A link is tens of bytes; a post with a caption is hundreds; media is megabytes. Lesson 3's arithmetic makes the third option absurd.
Freshness. Like counts change constantly. Links mean counts are fetched at read time from the sharded counters in Lesson 11; copies would mean rewriting every timeline containing the post.
Deletion. A removed post is filtered once at hydration rather than scrubbed from every timeline.
Fan out references, not values. The newsfeed chapter reached it, that building block reached it, and Lesson 6 reached it for media paths. This is the same rule applied to the timeline itself.
The blob overflow rule is a real pattern, and it has a subtlety
"If the value size exceeds the store's limit (e.g. a few MBs), store the actual timeline data in a blob store and keep a reference link in the key-value store."
This is worth knowing because it is a common, practical technique — key-value stores generally cap value size, and a few users will always exceed it.
Who overflows? A user following tens of thousands of accounts accumulates an enormous timeline. At, say, 50 bytes per link, a few megabytes is roughly 60,000 entries — rare, but not rare enough to ignore at a billion users.
The subtlety the design does not mention: this creates two read paths.
Normal: KV lookup -> timeline 1 round trip Overflow: KV lookup -> pointer -> blob fetch 2 round trips
So overflow users get systematically slower feeds — and they are, by definition, the users who follow the most accounts and are likely among the most engaged.
Two better options worth mentioning:
Cap the timeline. Nobody scrolls 60,000 entries. Store the most recent few hundred and fall back to on-demand generation for anything older. That bounds the value size by construction and removes the overflow case entirely.
Chunk it. Split the timeline across keys — user:123:page:0, page:1 — so pagination maps naturally onto chunks and no single value is oversized.
An overflow path that is slower than the normal path penalizes exactly your heaviest users. Bounding the object is usually better than handling the overflow.
Stories
We can add a "Story" feature where photos expire after 24 hours. We implement this by storing a timestamp with the story entry. A task scheduler or TTL mechanism automatically deletes entries that exceed the 24-hour limit.
Ephemerality contradicts the durability requirement, and that is the interesting part
Lesson 2's non-functional requirement says: "Any uploaded content should never get lost."
Stories are content the system has promised to lose.
Those are in direct conflict, and resolving it is instructive. The requirement is not system-wide — it becomes a per-object policy decided at write time:
| Permanent post | Story | |
|---|---|---|
| Durability | Maximum — multi-region, replicated | Deliberately limited |
| Lifetime | Indefinite | 24 hours |
| Storage tier | Durable, archival | Cheap, expiring |
| Backups | Yes | No — backups would defeat the promise |
That last row is the sharp one. If stories are included in routine backups, the content survives its own expiry — and a system that promises deletion while retaining copies has a real problem, legally and ethically.
So ephemerality is not just a TTL. It requires excluding the data from backup and replication policies designed to prevent loss, which means the storage tier itself must be different.
When a product adds ephemerality, "never lose data" stops being a system property and becomes a per-object policy — and the deletion promise has to reach into your backup strategy.
Task scheduler versus TTL — both work, and they differ
"A task scheduler or TTL mechanism automatically deletes entries."
They are not interchangeable, and the difference matters at Instagram's volume.
A TTL is a property of the store — set an expiry, the store stops returning the item and reclaims it during compaction. Cheap, automatic, and works well for small values in a key-value store.
A task scheduler runs a recurring job that finds expired entries and acts on them. More expensive, and necessary when expiry requires work beyond forgetting a key.
Here, expiry requires exactly that work: the story's media in blob storage must be reclaimed. A TTL on a metadata row does not delete a 3 MB photo from S3, and at hundreds of millions of stories a day, orphaned media accumulates fast.
So the honest answer is both:
TTL -> on the metadata entry, so reads stop returning it IMMEDIATELY Task scheduler -> a batch job that reclaims the underlying MEDIA
That ordering also gives the right user-visible behaviour: the story disappears at exactly 24 hours regardless of when the cleanup job next runs.
Expire the reference synchronously and reclaim the bytes asynchronously. This is why Lesson 4's block list includes a task scheduler that no requirement asked for.
Stories change the fan-out calculation too
The chapter treats Stories as an addition to storage, but it also affects Lessons 8 and 9.
A story is a post with a 24-hour lifetime. Push it to followers' timelines and you are fanning out content that will be deleted tomorrow — the write cost is identical to a permanent post, and the value expires.
Which suggests stories are a good candidate for pull, regardless of follower count:
- They are few per user — you fetch one account's stories, not a merged timeline.
- They are displayed as a separate row, not interleaved into the feed.
- They expire, so precomputation is wasted work on a short clock.
That matches how the feature actually appears in the product: a horizontal row of accounts, fetched when you look at it, rather than posts woven into a ranked feed.
Content with a short lifetime should be pulled, because precomputation only pays off across many reads and there is not much time for many reads.
Key takeaway
Timelines store a list of links, not posts — the same reference rule every chapter in this module reaches. The blob overflow path for oversized values is a real pattern but creates two round trips for the heaviest users; capping or chunking the timeline is usually better than handling the overflow. Stories contradict the durability requirement, which resolves by making "never lose data" a per-object policy — and the deletion promise must reach into backup strategy, or content survives its own expiry. Expiry needs both mechanisms: a TTL on the metadata so reads stop immediately, and a scheduler to reclaim the media — expire the reference synchronously, reclaim the bytes asynchronously. And short-lived content should be pulled, since precomputation pays off across many reads and there is little time for many.
Next: the finalized design, with the CDN in front.