Skip to content

Time-Series Database#

Problem statement (interviewer prompt)

Design a time-series database (Prometheus / InfluxDB / TimescaleDB). It must ingest 1M+ samples/s, compress to ~1 byte/sample, support range queries with aggregations, downsample for long-term storage, and gracefully reject high-cardinality writes.

flowchart LR
  W[Writers<br/>agents, services]
  ING([Ingest API])
  WAL[WAL]
  MEM[Mem buffer<br/>per-series]
  TSM[(Compressed TSM blocks)]
  Q[Query Engine]
  R([Reader / Dashboard])
  W --> ING --> WAL --> MEM
  MEM -. flush .-> TSM
  R --> Q --> TSM
  Q --> MEM

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
    class W,WAL,MEM,Q service;
    class TSM datastore;
    class ING compute;
    class R obs;
flowchart TB
  subgraph Sources[Sources]
    EXP([Exporters / SDKs])
    SCR[Pull scraper - Prometheus]
    AGT[Push agent - Telegraf / OTel]
    EDGE([Edge devices / IoT])
  end

  subgraph Ingest
    LB[Ingest LB]
    AUTH[AuthN / multi-tenant]
    REL[Relabel + filter]
    DEDUP[Dedup window]
    WAL[Per-tenant WAL]
  end

  subgraph Memory[Hot tier]
    SER[Series cache<br/>label-set → ID]
    MEM[In-memory chunks<br/>2h Prometheus / segment]
    INV[(Inverted index<br/>label → series ids)]
  end

  subgraph Compaction
    FLUSH[Flush to disk every 2h]
    HEAD[Head block]
    LEV[Compaction levels]
    DELTA[Delta + Gorilla compression]
    TSI[TSM / Parquet files]
  end

  subgraph Long[Long-term tier]
    LTS[(Thanos / Mimir / Cortex<br/>S3 + sidecar)]
    DOWN[Downsampling 5m / 1h / 1d]
    RETN[Retention policies]
  end

  subgraph Query
    PROMQL[PromQL / Flux / SQL]
    PLANNER[Query planner]
    EXEC[Range vector eval]
    AGG[Aggregations: rate, irate, histogram_quantile]
    RECORDING[Recording rules]
    ALERT[Alert rules]
  end

  subgraph Downstream
    DASH[Grafana]
    PD[PagerDuty / Opsgenie]
    SLO[SLO platform]
  end

  Sources --> LB --> AUTH --> REL --> DEDUP --> WAL --> MEM
  MEM --- SER
  MEM --- INV
  MEM -. FLUSH .-> HEAD --> LEV --> TSI
  TSI -. upload .-> LTS
  LTS --> DOWN
  Query --> MEM
  Query --> TSI
  Query --> LTS
  Query --> PROMQL --> PLANNER --> EXEC --> AGG
  Query --> RECORDING
  Query --> ALERT --> PD
  Query --> DASH

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
    class EXP,EDGE client;
    class LB edge;
    class AUTH,REL,DEDUP,WAL,SER,FLUSH,HEAD,LEV,DELTA,TSI,DOWN,RETN,PLANNER,EXEC,AGG,RECORDING service;
    class INV,PROMQL datastore;
    class LTS storage;
    class SCR,AGT,MEM,ALERT,DASH,PD,SLO obs;

Why TSDB needs a special engine#

  • Each metric series is identified by label set (name, {labels…}).
  • Most queries read a small subset of series over a wide time range.
  • Data is mostly written once, read many; values are floats; timestamps monotonic.
  • Compression: delta encoding for timestamps + Gorilla / XOR for floats can hit 1-2 bytes/sample.

Hot path layout (Prometheus)#

  • In-memory head block holds 2 hours of samples per series.
  • Inverted index maps labels → series IDs.
  • Series cache turns label hash → ID at ingest.
  • After 2 h, head block sealed and written to disk as immutable block.

Long-term & downsampling#

  • Single Prometheus retains weeks; for years use Thanos / Mimir / Cortex / VictoriaMetrics.
  • Downsample to 5 m / 1 h / 1 d for fast historical queries.

Cardinality is the killer#

  • Each unique label set = new series.
  • Putting user_id in a label can explode series count → memory crash.
  • Always cap cardinality at ingest; reject high-cardinality writes.

Glossary & fundamentals#

Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.

Tag Concept What it is Page
HLD CAP / PACELC C vs A under partition; L vs C otherwise cap-pacelc
HLD LSM vs B-Tree engines WAL, memtable, SSTables, compaction storage-engines-lsm-btree
HLD Observability metrics, logs, traces, SLOs observability
HLD Service mesh sidecar mesh, mTLS, traffic policy service-mesh
HLD Search internals inverted index, BM25, embeddings, ANN search-internals
LLD Testing strategy pyramid, doubles, TDD, contracts testing-strategy
LLD Immutability immutable types, persistent collections immutability

Quick reference#

Functional#

  • Ingest timestamped numeric (and sometimes string) samples tagged by labels.
  • Query with range, aggregations, downsampling.
  • Retention + downsampling rules.
  • Alerting on derived series.

Non-functional#

  • 1M+ samples/s per node (compressed format).
  • p99 query < 1 s for typical dashboard panels.
  • Retention from days (hot) to years (cold tier).

Capacity#

  • Active series per node: 1-10 M depending on RAM.
  • Disk: ~1-2 bytes / sample after Gorilla compression.

Schema#

  • Series = (name, label set) → unique ID.
  • Sample = (series_id, ts, value).
  • Inverted index: label=value → posting list of series IDs.

API#

# Prometheus push gateway / remote_write
POST /api/v1/write   protobuf
GET  /api/v1/query?query=rate(http_requests_total[5m])
GET  /api/v1/query_range

Trade-offs#

  • Cardinality limit: every prod TSDB lives or dies by it. Never label with user IDs.
  • Pull vs push: Prometheus pulls (good for service discovery); Influx pushes (good for short-lived jobs).
  • One backend vs federated: Thanos / Mimir federate many Prom shards behind a single query API.
  • Long retention = cold tier on object storage; query merges hot + cold via sidecars.

Refs#

  • "Gorilla: A Fast, Scalable, In-Memory Time Series Database" (FB, VLDB '15).
  • Prometheus storage docs; TimescaleDB chunking docs.
  • Thanos / Cortex / VictoriaMetrics architecture posts.

FAQ#

How do time series databases compress samples to about 1 byte?#

Gorilla style compression delta encodes timestamps and XOR encodes float values, exploiting the fact that adjacent samples in a series rarely differ much. This often achieves around 1.3 bytes per sample on average.

What is the role of the WAL in a TSDB?#

Incoming samples are appended to a write ahead log first, then buffered in an in memory per series structure. The WAL ensures durability so recent writes survive a crash before being flushed to disk.

How does downsampling work in a time series database?#

A background job aggregates raw samples into coarser resolutions like 5 minute and 1 hour rollups. Older data is queried at lower resolution, cutting storage and speeding up wide range queries.

Why is high cardinality dangerous for a TSDB?#

Each unique label combination creates a separate series with its own index entry and memory buffer. Unbounded labels like user_id or request_id can blow up memory and degrade query performance.

When should I pick a TSDB over a relational database?#

Pick a TSDB when ingest is append only, queries are range scans with aggregations over time, and data has natural retention. Stick with SQL for transactional workloads or rich joins.

Video walkthrough

Time Series Databases Explained: InfluxDB, Prometheus, TimescaleDB : via System Design Walkthrough