Skip to main content

Time-Series Databases

Level 2: Level 2: Data Storage & Modelingmedium30 mintime-seriesmetricscardinality

Exploit append-only time-ordered data with columnar compression, time-partitioning, and downsampling tiers, and design against cardinality explosion from unbounded tags.

Time is the primary axis

A time-series database (TSDB) is specialized for a distinct workload: append-heavy, time-ordered writes of measurements, where time is the primary axis of both storage and query. Metrics and monitoring (Prometheus), IoT sensor data (InfluxDB, TimescaleDB), observability, financial ticks, and anything that is fundamentally "value at timestamp, tagged by source" fits. Writes almost always append at the current time (you rarely update the past), reads are overwhelmingly range scans ("CPU for host X over the last hour"), and old data is queried less and less as it ages.

Why not just use Postgres?

You can start there, but three properties of the workload make a purpose-built engine win at scale.

1. Columnar storage + specialized compression. A time series is a column of numbers with regularly spaced timestamps, which compresses extraordinarily well stored column-wise. Delta-of-delta encoding on timestamps: if points arrive every 10 seconds, the delta is constant and the delta-of-delta is 0, packing to almost nothing. Gorilla / XOR compression on values (Facebook's Gorilla paper): consecutive float values are often close, so XORing them leaves mostly zero bits. Together these routinely hit 10x or better compression versus row storage, the difference between affordable and ruinous at millions of points per second.

2. Time-partitioned storage and retention. Data is written into time-bucketed partitions (chunks by day or hour). Range queries touch only the relevant chunks, dropping old data is an O(1) partition drop instead of a mass DELETE, and tiering puts recent hot data on fast SSD, warm data on cheaper disk, and ancient data downsampled or in object storage.

3. Downsampling and rollups. You do not keep raw per-second points forever. Retention policies plus rollups keep raw data for, say, 7 days, 1-minute aggregates for 30 days, and 1-hour aggregates for 2 years. A dashboard showing last-year trends reads the cheap hourly rollup, not billions of raw points.

Check yourself
A dashboard charts one year of CPU usage for a whole fleet. In a well-configured TSDB, which data does that query actually read?

The signature failure mode: cardinality explosion

This is the single thing interviewers test. A time series is identified by its metric name plus its set of tag/label key-value pairs: http_requests{host, region, status, endpoint, user_id}. The number of distinct series is the product of the distinct values of every tag. Add a high-cardinality tag like user_id (millions of values) or request_id (unbounded) and you multiply your series count into the millions or billions. Each distinct series needs its own index entry and storage stream, so cardinality explosion blows up index memory, slows every query, and can OOM the database. Prometheus falling over because someone added a user_id label is a real, common outage.

Check yourself

You are labeling the metric 'http_requests'. Sort each candidate label before the pager does it for you.

'status' (HTTP status code)
'region' (deployment region)
'endpoint' as a route template like '/users/:id'
'url' as the raw path with query parameters
'user_id'
'request_id'

Controlling cardinality is the core design skill: keep labels low-cardinality and bounded (host, region, status code, endpoint template), never put unbounded identifiers (user id, request id, full URL with query params, email) into labels. If you need per-user analytics, that belongs in an OLAP store (ClickHouse) or logs, not in a metrics TSDB. Use endpoint templates (/users/:id) not raw paths.

Interview nuance: Know the landscape. Prometheus is pull-based metrics with its own TSDB, great for infra monitoring, not for long-term high-cardinality analytics. InfluxDB / TimescaleDB (the latter is Postgres with time-series superpowers, so you keep SQL and joins) are general TSDBs. ClickHouse is a columnar OLAP database often used for high-cardinality, high-volume time-series analytics where you need arbitrary group-bys that would kill a label-indexed TSDB.

Recap: TSDBs exploit append-only, time-ordered, columnar data with delta-of-delta and Gorilla compression, time-partitioning, retention tiers, and downsampling to make metrics affordable, and the failure mode you must design against is cardinality explosion from unbounded tags.

Check yourself
Product asks for per-user request latency, and a teammate proposes adding a 'user_id' label to the existing Prometheus latency metric. What is the senior counter-proposal?

Apply

Your turn

The task this lesson builds to.

Design storage for a metrics/monitoring system ingesting millions of points/sec with fast recent-range queries and cheap long-term retention.

Think about

  1. Why is cardinality explosion the key failure mode?
  2. How do downsampling and retention tiers bound cost?
  3. Why is columnar + delta-of-delta compression a good fit?

Practice

Make it stick

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

Design the time-series storage for Datadog-style multi-tenant observability ingesting 20M points/sec across thousands of customers, where any one customer can accidentally emit a runaway high-cardinality metric that must not degrade other tenants. Explain your ingestion, cardinality guardrails, and how you isolate a noisy tenant.

Think about

  1. Where in the pipeline do you enforce cardinality limits so a runaway never reaches the index?
  2. What makes cardinality an isolation boundary rather than just a performance concern in multi-tenant?
  3. How do per-tenant quotas cover ingest, series count, and query cost?