Retention vs deletion — the index has to forget — delete-by-query tombstones and manual force-merge
Retention is when the system stops promising you can read; deletion is when the bytes are physically gone — and the index that points at the bytes is a separate structure with its own lifecycle, which is where compliance bugs live.
We have a cost ladder for where bytes live as they age. Aging through the ladder is the ILM policy; but the policy decides when bytes MOVE, not when bytes DIE — and 'die' has its own lifecycle.
Scene 08
Retention vs deletion — the index has to forget
- Watch
- Try it
- Predict
- Capture
Watch the two timelines. The TOP row is the data on disk — each cell is a day of records. A retention cut-off line sits at day 30: anything to its left is retention-expired but still physically present. The BOTTOM row is the index — it has its own aging clock. The compactor ticks daily; only AFTER it runs do bytes actually leave disk and only AFTER it runs does the index forget. In steady state both shrink in lockstep — but they are NOT the same lifecycle.
Highlighted lines are the ones running in the diagram right now.
# runs daily as part of the ILM delete actiondef expire(now):for index in cluster.indices:# per-index policy; templates can override cluster defaultwindow = index.template.retention or cluster.default_retentionfor record in index.records:age = now - record.ingest_dayif age > window:record.retention_expired = True # promise dropped# NOTE: bytes still on disk until Compactor.run()
# runs daily; this is when bytes physically LEAVE diskdef run():for segment in index.segments:if segment.size_gb > max_merged_segment: # 5 GB defaultcontinue # SKIP — too big to auto-mergekept = [r for r in segment.recordsif not r.retention_expiredand not r.tombstoned]new_segment = rewrite(kept) # bytes finally gonereplace(segment, new_segment)compactor.last_run_day = today()
def execute(query):# segments are immutable — no in-place removalmatches = index.search(query)for doc in matches:segment = doc.segmentsegment.tombstones.add(doc.id) # marker, not eviction# index now returns 0 hits for query (the promise)# but bytes stay until segment.merge() rewrites it# auto-merge SKIPS segments > max_merged_segment (5 GB)return {deleted: len(matches), bytes_freed: 0}def force_merge(index): # operator-invoked; IO-heavy, blockingfor segment in index.segments:if segment.size_gb > max_merged_segment:continue # default: still skips >5 GBrewrite_without(segment, segment.tombstones)
Where this sits in Build a distributed logging stack (ELK / Loki)
Scene 08 of 12. Retention is when the system stops promising you can read; deletion is when bytes are physically gone — and the gap is where compliance bugs live.
Up next. Bytes die on a schedule and the index has to forget. But even with perfect retention, at scale we cannot keep everything we emit. The honest question is which lines we sacrifice — and the wrong answer is 'the only error of the day'.
All 12 scenes in Build a distributed logging stack (ELK / Loki) · Every curriculum