Topic 15

TSDB Internals

TSDB

Everything Prometheus scrapes ends up under --storage.tsdb.path on obs-01's local disk, and the layout is worth understanding before you size it, back it up, or trust it. The design has two moving parts: an in-memory head block journaled to a write-ahead log, and behind it a growing shelf of immutable two-hour blocks. That shape is why a restart — or a crash — hands you back your data.

Nothing in this topic is Harborline-specific, but the numbers are: six node_exporters plus the store exporters push roughly 10,000 active series through this machinery every 15 seconds. Small numbers, deliberately — the point of walking the arithmetic now is that it stays linear when Chapter 5 multiplies the series count.

The Head Block and the WAL

Incoming samples land in memory, in the head block, and are simultaneously appended to a write-ahead log on disk, segmented into 128 MB files. The WAL is never queried. It exists for exactly one reader: a restarting Prometheus, which replays it to rebuild the head as it was.

Kill the process mid-scrape and you lose essentially nothing — you wait through the replay instead. That replay is the real cost of a restart: minutes on a large head, during which nothing is scraped. Plan around the gap, not around data loss.

Two-Hour Blocks

Roughly every two hours, the oldest head data is written out as a block: a directory named by ULID containing compressed chunks, a full inverted index, and a meta.json describing its time range. Once written, a block is never modified. Queries read the head plus whichever blocks the time range touches, and immutability is what keeps the engine simple to reason about — and, as the backup discussion below shows, cheap to snapshot.

# ls /prometheus — the whole database is this directory tree
01JZX0Q8Z3N9K5T2W7E4R6M1BC/   # one immutable ~2 h block
  chunks/000001                # compressed samples
  index                        # inverted index: labels -> series
  meta.json                    # time range, stats, compaction level
chunks_head/                   # the head's memory-mapped chunks
wal/00000012                   # 128 MB write-ahead segments

The listing is the whole contract: immutable block directories, the head's memory-mapped chunks, and the WAL segments that make the head crash-safe. Everything Prometheus knows lives in this one tree on local disk — which is also why the storage rules later in this topic are unforgiving about what kind of disk.

Compaction

A background compactor merges small blocks into larger ones, deduplicating index entries and applying deletion tombstones as it goes. Block size has a ceiling: 10% of the retention window or 31 days, whichever is smaller — at the default 15-day retention Harborline runs, that computes to 36 hours, but Prometheus only compacts in fixed 2h→6h→18h→54h steps, so blocks actually top out at 18 hours. Compaction is why a query spanning a week touches a handful of block indexes instead of eighty-four.

Retention

Two limits, enforced together: --storage.tsdb.retention.time, default 15 days, and --storage.tsdb.retention.size — whichever trips first wins. Deletion is coarse. Entire expired blocks are dropped at compaction time, so data leaves in multi-hour slabs, and there is no per-series or per-age policy inside one server.

A sample's life in the TSDB — head to the retention cliff
Head + WAL
in memory, journaled to disk
Two-hour block
immutable ULID directory
Compaction
merge into larger blocks
Retention
whole blocks dropped at 15 days
no downsampling · full resolution to the end

The Sizing Arithmetic

Compression brings a sample to roughly 1–2 bytes on disk, and the sizing formula is one line: needed disk ≈ ingestion rate × bytes per sample × retention. Ingestion rate is active series divided by scrape interval — every term is observable on a running server.

Harborline's chapter-end state: call it 10,000 active series every 15 s, which is about 670 samples per second, under 100 MB a day, roughly 1.5 GB for the full 15 days. A rounding error on any disk. The reason to run the arithmetic at this size is that it is linear — when Chapter 5 instruments five services and the series count multiplies, the same three numbers produce the new answer.

Full Resolution to the Cliff

Prometheus never downsamples. A 14-day-old sample sits at the same 15-second resolution as one from this minute, and on day 15 its block is deleted whole — full fidelity, then the cliff. The graceful thinning of old data that Graphite's Whisper and RRDtool did is deliberately absent, and it reappears only in the long-term systems of topic 16, where it belongs to someone else's storage engine.

Common Mistakes
  • Putting the data directory on NFS — the TSDB requires a POSIX-compliant local filesystem, and network filesystems are explicitly unsupported. The failure mode is not slowness but silent corruption, discovered at replay time.
  • Backing up by copying the live data directory — the head and WAL are mid-flight and the copy is inconsistent. The supported path is the snapshot API (POST /api/v1/admin/tsdb/snapshot with --web.enable-admin-api), which hard-links immutable blocks into a consistent directory at near-zero cost.
  • Deleting old blocks with rm on a running server — Prometheus's view of the block list desyncs from disk, and queries or compaction can fail. Retention flags and the delete-series admin API are the two sanctioned exits for data.
  • Cranking retention to a year and calling long-term storage solved — the disk math may work, but with no downsampling a year-wide query grinds through every raw sample, and a single unreplicated disk now holds a year of history. Past a few weeks, remote storage (topic 16) is the honest answer.
  • Assuming a restart loses recent data and building rituals around never restarting — the WAL makes restarts lossless. The real cost is replay time, minutes on a large head, during which nothing is scraped; that gap is what to plan around.
Best Practices
  • Give the TSDB a local SSD and size it with the bytes-per-sample arithmetic, then add 50% headroom — disk-full is the one storage failure that stops ingestion entirely.
  • Set retention.size alongside retention.time as a hard cap, so a cardinality accident fills the retention budget instead of the filesystem.
  • Back up via the snapshot API on a schedule if the history has value — block immutability makes snapshots cheap, and "we can re-scrape it" is false for history.
  • Watch the TSDB's own metrics — prometheus_tsdb_head_series for live cardinality, plus the WAL-corruption and compaction-failure counters. Prometheus is the first system it should be monitoring.
Comparable toolsSuccessors VictoriaMetrics, Thanos/Mimir — where data goes when it outgrows one disk (topic 16)Same lineage InfluxDB TSM — the same LSM-flavored write-then-compact designAncestors Whisper (Graphite), RRDtool — fixed-size, downsample-as-you-go, the rejected tradeoff

Knowledge Check

The OOM killer takes down Prometheus mid-scrape. What happens to the last hour of samples?

  • Lost — the head block lived only in memory
  • Recovered by replaying the write-ahead log
  • Flushed to an emergency block as the process dies
  • Re-scraped from the targets, which buffer an hour of data

Why does the snapshot API cost almost nothing, even on a large database?

  • It downsamples the data as it copies
  • It recompresses the chunks with a faster codec to shrink the snapshot
  • It only snapshots the WAL, which is small
  • Immutable blocks can be hard-linked instead of copied

A dashboard queries data from 14 days ago. What resolution does it get, with default settings?

  • The original 15 s scrape resolution, unchanged
  • Hourly averages — old data is progressively thinned
  • Whatever resolution compaction reduced it to
  • None — data that old was already deleted

What failure does setting retention.size alongside retention.time actually prevent?

  • WAL corruption during unclean shutdowns
  • A cardinality accident filling the filesystem
  • Slow queries over wide time ranges
  • Long WAL replay after restarts

You got correct