Skip to main content

Time-Series Databases & Storage Design

Level 11: Level 11: Specialized & Frontier Systemshard35 mintime-seriescardinalitydownsampling

A TSDB exploits append-only, columnar, delta-of-delta + XOR compressed storage partitioned by time, keeps old data cheap with downsampling and hot/warm/cold tiering plus retention, serves time-range + tag-filtered aggregations, and lives or dies by controlling tag cardinality.

A lopsided workload a general DB handles badly

Level 2's "Time-Series Databases" lesson introduced the TSDB and its append-heavy workload; this lesson credits that first pass and goes deep on cardinality, compression, and lifecycle. A time-series database (TSDB) is specialized because time-series workloads have a lopsided shape a general-purpose DB handles badly: writes are almost entirely appends at the current timestamp (you rarely update the past), the write rate is enormous (millions of points/sec), reads are time-range scans over a filtered set of series ("CPU for these hosts over the last 6 hours"), and old data is queried less and less over time. A B-tree row store like Postgres chokes here because random-position index maintenance under a pure-append firehose is wasted work.

Cardinality is the dominant failure mode

A series is identified by a metric name plus a set of key/value tags/labels, for example cpu_usage{host="web-1", region="us-east", pod="abc"}. Each unique combination of tag values is a distinct series with its own timeline. This is the single most important concept in the whole topic: cardinality is the number of distinct series, and cardinality explosion is the dominant failure mode. Put a high-cardinality tag like user_id, request_id, pod_uuid, or email on a metric and you can go from thousands of series to tens of millions, blowing up the in-memory index, slowing every query, and OOM-killing the database. The rule: tags must be bounded, low-cardinality dimensions (region, host, status code), never unbounded identifiers.

Append-optimized, columnar, compressed storage

TSDBs use LSM-tree style storage (buffer writes in memory, flush sorted immutable chunks to disk) and store data columnar per series so a range scan reads one contiguous block. Compression is where TSDBs win big, using two Gorilla/Facebook techniques:

  • Delta-of-delta on timestamps: samples arrive at near-regular intervals, so store the change in the interval, which is usually 0 and packs into a bit or two instead of a 64-bit timestamp.
  • XOR compression on values: consecutive float values are similar, so XOR them and store only the changed bits.

Together these routinely get metrics down to around 1 to 2 bytes per sample versus 16 raw, which is what makes million-point-per-second ingestion economically possible.

write path: memory buffer (recent, WAL-backed) --flush--> compressed columnar chunks
partition by TIME (e.g. 2h blocks) and by SERIES/shard
query: pick time chunks -> filter series by tags via inverted index -> scan + aggregate -> gap-fill

Keeping old data cheap

Downsampling / rollups (continuous aggregates): you do not need per-second data from last year, so precompute 1m, 1h, 1d rollups and serve old queries from the coarse ones. Tiering + retention: recent raw data lives on fast SSD (hot), older rolled-up data on cheaper disk/object storage (warm/cold), and raw data past its retention window is dropped entirely. Partitioning by time makes this trivial: expiring old data is dropping whole chunks, not deleting rows.

Query patterns you must support: time-range scans, tag filters (served by an inverted index from tag to series), aggregation across series (sum/avg/percentiles), and gap-filling / interpolation for missing samples. The ecosystem: Prometheus (pull-based monitoring, its own TSDB), InfluxDB and TimescaleDB (a Postgres extension, so you keep SQL and joins), and ClickHouse (a columnar OLAP DB people push into service as a huge-scale TSDB).

Interview nuance: if asked "why not just use Postgres," answer with write pattern (append vs random-write index churn), compression (delta-of-delta/XOR vs generic), and lifecycle (drop-a-time-chunk vs DELETE-scan). If asked "what breaks first at scale," the answer is cardinality, every time.

Recap: a TSDB exploits append-only, columnar, delta-of-delta + XOR compressed storage partitioned by time, keeps old data cheap with downsampling and hot/warm/cold tiering plus retention, serves time-range + tag-filtered aggregations, and lives or dies by controlling tag cardinality.

Apply

Your turn

The task this lesson builds to.

Design a time-series store for high-frequency sensor metrics that ingests millions of points/sec and serves fast time-range + downsampled queries.

Think about

  1. Why is tag/label cardinality the dominant failure mode?
  2. How do downsampling and tiering keep old data cheap?
  3. Why is columnar + delta-of-delta compression a fit?

Practice

Make it stick

A second problem on the same idea, so it survives past today.

Design the metrics backend for a Datadog-scale observability product: 100M+ active time series across thousands of customers, ingesting 10M+ points/sec, serving p99 dashboard queries under 1s over the last hour and ad-hoc queries over 15 months, all multi-tenant. Deliver the storage layout, how you keep 100M series from melting the index, and the query/retention strategy.

Think about

  1. How does sharding by (tenant, series) isolate a noisy customer?
  2. How do per-tenant active-series limits and label budgets bound index RAM?
  3. How does the query path serve sub-1s recent-hour and 15-month queries differently?