Skip to content
About
Main content

State TTL and expiry

How does state expire, and why is a TTL a silent correctness change?

Unbounded state is always a missing or unenforceable retention rule, so every backend grows expiry machinery. TTL (time to live) means "delete this entry when it has not been touched, or has existed, for a set time." The mechanisms trade thoroughness against cost:

MechanismHow it worksCostWeakness
Lazy cleanup on accesscheck expiry when an entry is readfreenever touches keys that are never read again, which are exactly the leaking ones
Background incremental scanscan a slice of state in each cyclebounded per cyclea continuous tax on CPU and I/O
Cleanup during drop expired entries while compaction rewrites filescheapest thorough option, as it rides on work already scheduledonly as often as compaction runs
Filter during snapshotskip expired entries when writing a smallstate in memory is not reduced

Cleanup during compaction is the same tombstone-collection trick that storage engines use, relocated into the runtime.

The semantics matter more than the mechanism

  • TTL on the last write expires keys that are updated rarely but still read often.
  • TTL on the last access keeps "zombie" keys alive forever, because something touches them periodically.
  • Event-time TTL is the correct one and depends on progress. So an idle or lagging source stops cleanup exactly when you need it.

TTL is a silent correctness change

This is the important property. A join whose state expires below the real time skew between the two streams produces missing rows, not errors. Nothing in the system distinguishes "no match existed" from "the match was evicted." The job assumes that real-world event skew stays inside the configured . It cannot verify that assumption. When it breaks, the cost lands on a downstream consumer who sees a slightly smaller number and has no signal at all.