Open-Source Wikis

/

Prometheus

/

Subsystems

/

Head and append path

prometheus/prometheus

Head and append path

Active contributors: jesusvazquez, codesome, krajorama, bwplotka

The Head is the in-memory portion of the TSDB. It holds every active series with its in-progress chunk plus a small ring of older mmapped chunks. New samples enter through an Appender, are buffered, and on Commit() are written to the WAL and inserted into the head.

Key types

Type / Function File Role
Head tsdb/head.go Top-level head structure: stripe-locked series map, WAL handle, exemplar store.
stripeSeries tsdb/head.go Sharded map[storage.SeriesRef]*memSeries to reduce lock contention.
memSeries tsdb/head.go Per-series state: labels, head chunk, mmapped chunks, last sample, last commit.
headAppender tsdb/head_append.go V1 appender. Buffers AppendOps; Commit writes WAL + inserts.
headAppenderV2 tsdb/head_append_v2.go V2 appender; one AppendEntry per call, supports ST/exemplars/metadata in one op.
Head.gc() tsdb/head.go Periodically drops expired series and chunks.
Head.Truncate(t) tsdb/head.go After a block is written, drops everything older than t.
Head.WAL() tsdb/head.go Returns the underlying *wlog.WL (head + sample records).
isolation tsdb/isolation.go Append/read isolation tracker; provides appendID + readSnapshot.

Append path V1

sequenceDiagram
    participant Scrape as scrapeLoop
    participant App as headAppender
    participant Stripe as stripeSeries
    participant WAL
    Scrape->>App: Appender(ctx)
    Scrape->>App: Append(ref?, labels, t, v)
    App->>Stripe: getOrCreate(labels)
    Note right of App: Sample buffered in app.samples
    Scrape->>App: Commit()
    App->>App: Validate (out-of-order, dups, NaN handling)
    App->>WAL: Log() series + samples in batches
    App->>Stripe: memSeries.append(t, v)
    App->>App: release isolation appendID

V1 is the default and the only path used by the older Storage.Appender(ctx) API. Notes:

  • The Append(ref, labels, t, v) returns a SeriesRef. Callers should store it and pass it back on subsequent appends to bypass the labels-lookup hot path.
  • AddFastNoCheck (head_append.go) skips out-of-order checks for the common scrape case where samples are guaranteed monotonic per series.
  • Metadata (MetricType, Help, Unit) is appended via UpdateMetadata.
  • Native histograms and float histograms have their own append methods (AppendHistogram, AppendHistogramCT).

Append path V2

V2 is the newer shape, defined in storage/interface_append.go::AppenderV2 and implemented in tsdb/head_append_v2.go. It is being rolled out as the canonical path; the migration tracker is issue #17632. Differences:

  • A single AppendEntry carries timestamp, value, optional histogram, optional exemplars, optional start timestamp, optional metadata.
  • Always-on metadata: every entry can update the type/unit/help on the series.
  • Exemplars are attached per sample, not separately appended.
  • Out-of-order handling is consolidated.

scrape/scrape_append_v2.go is the scrape-side counterpart; remote write write_handler.go and the OTLP handler also implement V2.

Stripe locking

stripeSeries is an array of 16 (configurable via --storage.tsdb.stripe-size) sub-maps, each with its own RWMutex. Look up a series by hashing its labels modulo the stripe count. This caps contention to the worst-case-per-stripe rather than serializing all writes.

Isolation

tsdb/isolation.go implements lockless multi-version isolation:

  • Each appender gets a monotonically increasing appendID.
  • Each query takes a snapshot listing the open appendIDs plus the high-water mark.
  • A read seeing a chunk produced by an in-progress append (whose appendID is in the open set) skips that chunk.
  • appendID and readSnapshot are atomic operations; no per-series locks are needed for the isolation check.

You can disable isolation with -test.tsdb-isolation=false. CI runs both with and without it.

Out-of-order samples

When out_of_order_time_window > 0 (in tsdb config) the head accepts samples whose timestamp is within the window but older than head.maxt. These are stored in a separate OOO head (tsdb/ooo_head.go) and merged with the regular head on read (tsdb/ooo_head_read.go). At compaction time the OOO chunks are written to overlapping blocks; vertical compaction then merges them.

The OOO head has its own append path (Head.appendOOOHistogram, etc.) and its own metric set (prometheus_tsdb_ooo_*).

Chunk lifecycle in the head

A series' active chunk lives in memory. When it fills (default 120 samples) or its time range exceeds the chunk range:

  1. The chunk is finalized and added to memSeries.mmappedChunks.
  2. The bytes are appended to a chunks_head/NNNNNNN segment file (tsdb/chunks/).
  3. The segment is mmapped and the in-memory representation drops the bytes — only an offset is retained.
  4. A new in-progress chunk is allocated for new samples.

The mmap budget is bounded by --storage.tsdb.head-mmap-budget (3.x). When exceeded, mmaps are unmapped on a least-recently-used basis.

Memory snapshot on shutdown

With --enable-feature=memory-snapshot-on-shutdown, Head.Snapshot() writes a serialized form of the in-memory state to chunks_head/snapshot.NNNNNNN/ so the next start can restore series and chunks without replaying the WAL. See docs/feature_flags.md for the rationale.

Entry points for modification

  • Tweak the head append validation: headAppender.appendable() and friends in head_append.go.
  • Add a sample type: wire it through both V1 and V2, the WAL record (tsdb/record/record.go), and the corresponding chunk encoding (tsdb/chunkenc/).
  • Adjust chunk-cut policy: memSeries.appendPreprocessor() and chunkenc.MaxBytesPerXORChunk (or its equivalents).

See WAL for the durability layer, Blocks for what happens after compaction, and Storage for the interface contracts.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Head and append path – Prometheus wiki | Factory