Expert Track · Phase J · 14 of 26
Beyond OLTP, OLAP, vector, and search — time-series workloads have specific characteristics that demand purpose-built infrastructure.
Module 60 · Expert 14 / 26 · 90 min

Time-series
databases.

The specific engineering that ingests a million points per second, compresses them 12× via Gorilla encoding, aggregates over 30-day windows in sub-second, downsamples raw ticks to 5-minute rollups automatically, and retains only what the retention policy allows. Prometheus + VictoriaMetrics — label-based, pull-model, observability standard, PromQL. TimescaleDB — Postgres extension, hypertables auto-partitioned by time, continuous aggregates for materialized rollups, SQL-native with time-series superpowers. InfluxDB 3.0 + ClickHouse — columnar, analytical, extreme-scale, blurring into OLAP. Understanding these three architectures — and when each fits — is Expert-tier competence for observability, IoT, and financial time-series infrastructure.

// What you\'ll know by the end

  • Time-series storage internals (WAL + chunks + Gorilla)
  • Label-based (Prometheus) vs hypertable (Timescale) vs columnar
  • Retention + downsampling + continuous aggregates
  • Observability stack (Thanos, Cortex, Mimir at scale)
§ 01 — Why time-series is its own paradigm

1 million points
per second in.
30-day rollups
out in 50ms.

Time-series workloads have specific characteristics that make general-purpose databases (Postgres OLTP, Snowflake OLAP) categorically wrong for them. Consider what a real observability system does: millions of servers, containers, functions each emit metrics every 10-15 seconds (CPU %, memory, request rate, latency percentiles, error rate). At 10,000 servers × 100 metrics/server × 1 sample/15s, that\'s 66,000 writes per second continuously. Financial market data at 200 million ticks per trading day (S&P 500 alone). IoT sensor networks at millions of devices reporting every minute — billions of points per day. Postgres OLTP with a `(metric_name, timestamp, value)` table would collapse: (a) B-tree indexes fight write-heavy patterns (write amplification, index bloat); (b) tables grow into billions of rows (query time explodes); (c) aggregation over 30-day windows scans billions of rows (multi-minute latency). Snowflake/BigQuery would work but at 10-100× the cost — analytical warehouses aren\'t designed for continuous high-frequency ingestion. The specific engineering task: purpose-built TSDBs use timestamp as primary axis, LSM-tree write path for extreme ingestion, Gorilla compression (Facebook 2015) for 12× storage reduction, automatic downsampling to keep old data queryable without cost explosion, and retention policies to drop data past N days. Prometheus/VictoriaMetrics dominate observability. TimescaleDB brings SQL familiarity with hypertables. InfluxDB 3.0 and ClickHouse extend to analytical-scale time-series. Understanding these three architectures is Expert-tier competence for modern data infrastructure — because every serious application generates time-series (metrics, logs traces, events, IoT, financial ticks), and every serious infrastructure needs to store, query, and retain them efficiently.

// TIME-SERIES CHARACTERISTICS · WHY PURPOSE-BUILT MATTERS
TIME-SERIES WORKLOAD PROFILE · WHERE POSTGRES + SNOWFLAKE BOTH FAIL INGESTION PROFILE continuous · high-frequency · append-only → 100K-10M writes/sec sustained → (metric_name, labels, ts, value) → monotonic timestamps (mostly) → append-only (no updates) → float64 or int64 values QUERY PROFILE range scan · aggregation · time-windowed → range scans over time windows → SUM/AVG/MAX/percentiles → group by 5m / 1h / 1d buckets → typical: last 24h, 7d, 30d → sub-second latency required STORAGE PROFILE · WHY GORILLA COMPRESSION MATTERS delta-of-delta timestamps (4× compression) + XOR-encoded values (3× compression) = 12× total Facebook Gorilla 2015 · Prometheus/VictoriaMetrics/InfluxDB all use variants · 16-byte point → 1.3 bytes LABEL-BASED TSDB observability standard → Prometheus (pull-model) → VictoriaMetrics (compatible) → Thanos/Cortex/Mimir → PromQL query language HYPERTABLE SQL SQL-native time-series → TimescaleDB (Postgres ext) → auto-partition by time → continuous aggregates → full SQL + joins to dims COLUMNAR ANALYTICAL extreme scale + analytics → InfluxDB 3.0 (Iceberg) → ClickHouse (analytical DB) → columnar Parquet storage → blurs TSDB / OLAP line
Time-series workloads have specific characteristics that demand purpose-built infrastructure. Ingestion profile: 100K-10M continuous writes per second — orders of magnitude beyond typical OLTP; append-only (rare updates); monotonic timestamps (data arrives in time-order mostly); small fixed-size values (float64 or int64). Postgres B-tree indexes fight this pattern (write amplification, WAL saturation, index bloat). Query profile: range scans over time windows (last 5m / 1h / 24h / 7d / 30d); aggregation with SUM/AVG/MAX/percentile; group-by time buckets; sub-second latency required for dashboards. Postgres scans billions of rows; Snowflake charges per-query. Storage profile: without specialized compression, a year of 1-second metrics = 31M points × 16 bytes = 500MB per metric. With Gorilla compression (Facebook 2015 paper — delta-of-delta timestamps + XOR values), it compresses to ~40MB — 12× reduction. Prometheus, VictoriaMetrics, InfluxDB all use Gorilla variants. Storage cost + query speed both benefit. Three architectural approaches: (a) LABEL-BASED (Prometheus, VictoriaMetrics) — metric identified by name + labels {job, instance, method, status}; excellent for observability where high-cardinality label combinations matter; PromQL query language. (b) HYPERTABLE SQL (TimescaleDB) — Postgres extension; hypertables auto-partition tables by time; continuous aggregates materialize rollups; joins to dimensional tables via SQL. Best when time-series lives alongside relational data. (c) COLUMNAR ANALYTICAL (InfluxDB 3.0 on Iceberg/Parquet + DataFusion; ClickHouse with time-series-optimized materialized views) — analytical-scale time-series; blurs into OLAP. Best for petabyte-scale time-series analytics. Understanding when each architecture fits is Expert-tier data infrastructure competence.

The specific engineering task M.60 addresses is understanding how time-series storage engines work, why they differ from OLTP and OLAP, and how the three canonical architectures (label-based TSDB, hypertable SQL, columnar analytical) fit different workloads. The critical insight: time-series is not "just data with timestamps." It\'s a specific workload pattern with continuous high-frequency append-only ingestion, timestamp-oriented range scans, temporal aggregation, retention/downsampling. Modern purpose-built TSDBs optimize the entire storage engine around these patterns: (a) Timestamp as primary partitioning axis — data physically clustered by time; range scans read contiguous blocks; irrelevant time ranges skipped entirely. (b) LSM-tree write path — writes go to memtable (in-memory buffer) + WAL for durability; memtable flushes to immutable chunks; chunks compact over time. Sustains millions of writes/sec. (c) Gorilla compression (Facebook 2015) — delta-of-delta timestamp encoding (most timestamps are exactly 10s or 15s apart from previous: encode delta 15, then delta-of-delta 0, 0, 0... = 1 bit per timestamp!) + XOR value encoding (consecutive values often share leading/trailing zeros in float64 representation). 12× compression on typical time-series. (d) Automatic downsampling — raw data for last 24h; 1-minute rollups for last 7d; 1-hour rollups for last 90d; 1-day rollups for last 2y. Query old data at coarser granularity fast + cheap. (e) Retention policies — data older than N days automatically dropped. Cost bounded. (f) Time-aware query language — PromQL (Prometheus), Flux (InfluxDB 2.x), SQL with time_bucket() (TimescaleDB, InfluxDB 3.0). Native support for temporal aggregations, gap-filling, time-windowed joins. Each purpose-built system implements variants of these mechanisms optimized for their target workload. Understanding the underlying engineering is Expert-tier competence.

// FOUR APPROACHES TO TIME-SERIES STORAGE · WHERE EACH FAILS OR FITS
Attempt 1: Postgres OLTP with (metric, ts, value) table// works small · breaks at 10K writes/sec
"Just use Postgres. Create table metrics (metric text, ts timestamptz, value float8). Add index on (metric, ts). Done." Works for small workloads (thousands of writes/sec, millions of rows total). Standard SQL access; joins to dimensional tables; ACID transactions. But Postgres wasn\'t designed for time-series patterns. The specific failures at scale: (a) B-TREE INDEX WRITE AMPLIFICATION — each insert rebuilds relevant B-tree pages; at 10K writes/sec, index maintenance saturates disk I/O. WAL grows massively (every insert + index update logged). Autovacuum can\'t keep up. (b) TABLE SIZE EXPLOSION — 10K writes/sec × 86400s/day = 864M rows/day = 25GB/day (compressed as-is). Year = 315B rows = 9TB. Query planner\'s statistics degrade; index scan performance degrades. (c) NO NATIVE DOWNSAMPLING — user must build cron jobs to compute rollups; complex; error-prone; storage grows unbounded. (d) NO NATIVE RETENTION — must manually DELETE old rows; DELETEs on Postgres are expensive (vacuum required). Partition tables help but manual. (e) AGGREGATION QUERIES ARE SLOW — SUM/AVG over 30 days scans billions of rows; without materialized views, 30+ seconds. Not usable for dashboards. Standard failure mode for time-series in vanilla Postgres above ~10K writes/sec sustained.// FAIL MODE: B-tree contention · index bloat · unbounded growth · slow aggregation
TOY SCALE
Attempt 2: OLAP warehouse (Snowflake / BigQuery / Redshift)// analytical fits · expensive continuous ingestion
"Use Snowflake / BigQuery. Columnar storage; excellent aggregation performance; scales to petabytes." Analytical queries perform beautifully — SUM over 30-day windows in seconds even on billions of rows; column-oriented storage compresses time-series well; SQL familiar. But specific limits: (a) INGESTION COST — Snowflake / BigQuery charge per credit/slot or per warehouse hour; continuous 100K+ writes/sec is expensive. Streaming ingestion (Snowflake Snowpipe, BigQuery Streaming API) works but costs $10K-100K+/month at time-series volumes. (b) INGESTION LATENCY — batch loads have minute-scale latency; streaming has second-scale latency. Real-time dashboards need sub-second freshness — analytical warehouses can\'t deliver. (c) NO NATIVE DOWNSAMPLING/RETENTION — must build via scheduled queries; not automatic. Cost accumulates. (d) OVERKILL FOR OBSERVABILITY — most metrics queries are simple aggregations over last few hours; paying warehouse costs for this workload is 10-100× overspend. (e) NO PROMQL / TIME-SERIES-NATIVE LANGUAGE — teams comfortable with PromQL/Grafana ecosystems need to relearn. Standard for analytical time-series (historical financial analysis, large-scale retrospective analytics) but wrong for observability, IoT ingestion, high-frequency real-time monitoring.// FAIL MODE: expensive continuous ingestion · minute-scale latency · overkill for observability
EXPENSIVE +
LATENT
Attempt 3: RRDtool / Graphite / OpenTSDB// foundational · pre-modern · schema-rigid
"Use RRDtool / Graphite / OpenTSDB. Proven infrastructure; scales OK; battle-tested." The foundational monitoring stack. RRDtool (1999, Tobi Oetiker) — round-robin database; fixed-size ring buffer per metric; automatic downsampling built-in; native to Cacti/Munin/Nagios. Fantastic idea for its era (predictable disk usage, automatic aging) but rigid: schema fixed at creation (add a metric later? rebuild); no ad-hoc queries (fixed retention buckets); no rich tagging. Graphite (2003, Chris Davis) — Whisper storage format (RRD-inspired); metric names encoded as dotted paths (`webapp.server1.cpu.usage`); Carbon daemon for ingestion; graphite-web for queries. Widely deployed 2005-2015; superseded by Prometheus for new deployments. Whisper suffers from write amplification and schema rigidity. OpenTSDB (2011, StumbleUpon) — HBase-based; scales further; tag-based metrics (early move toward Prometheus\'s label model); complex ops (HBase = ZooKeeper + HDFS + region servers). All three were foundational but have been superseded by Prometheus/VictoriaMetrics/InfluxDB/TimescaleDB with better compression (Gorilla), more flexible schemas (labels), simpler ops (single binary), better query languages (PromQL, Flux, SQL). Legacy deployments still exist; new deployments should use modern purpose-built TSDBs.// FAIL MODE: schema-rigid · pre-Gorilla compression · superseded by modern TSDBs
HISTORICAL
Attempt 4: Purpose-built modern TSDB// Prometheus / TimescaleDB / InfluxDB 3.0 / ClickHouse · modern production
"Use a purpose-built TSDB matched to workload: Prometheus/VictoriaMetrics for observability; TimescaleDB for SQL-native + joins; InfluxDB 3.0 or ClickHouse for petabyte-scale analytical time-series." The specific modern engineering. Specifically: (a) Prometheus + VictoriaMetrics (label-based): pull-based scraping (Prometheus polls targets); label-based metric identification (metric{job="api", instance="node1", method="GET"}); PromQL query language; Gorilla compression; native downsampling via recording rules; single-binary ops (VictoriaMetrics one process, Prometheus one). Standard observability stack. Grafana as visualization frontend. Scales to millions of active series per node. (b) TimescaleDB (hypertable SQL): Postgres extension; `SELECT create_hypertable(\'metrics\', \'ts\')` transparently partitions the table by time; native SQL for queries; continuous aggregates materialize rollups automatically (`CREATE MATERIALIZED VIEW ... WITH (timescaledb.continuous)`); compression via native columnar format for older chunks (10× vs uncompressed); retention policies (`SELECT add_retention_policy(\'metrics\', INTERVAL \'90 days\')`); joins to dimensional tables via standard SQL. Best when time-series alongside relational data. (c) InfluxDB 3.0 (columnar): complete rewrite (2023) on Apache Iceberg + Parquet + DataFusion; separates storage from compute; SQL + InfluxQL; petabyte-scale; blurs TSDB/OLAP. (d) ClickHouse: analytical column store adapted for time-series; excellent compression (Gorilla + LZ4 + ZSTD); materialized views for aggregations; SQL; scales extreme (billions of writes/sec at Yandex, Cloudflare). (e) Observability at scale (Thanos, Cortex, Mimir): multi-tenant Prometheus over object storage (S3/GCS/Azure Blob); global query view across regions; long-term retention. Standard for enterprise observability.// FIT: match architecture to workload · Prometheus / Timescale / Influx3 / ClickHouse · modern production
PRODUCTION
MODERN
// THE COMPOSITE PATTERN

Each earlier attempt fails specifically. Postgres OLTP collapses under continuous high-frequency ingestion. OLAP warehouses are expensive + latent for observability workloads. RRDtool/Graphite were foundational but superseded by Gorilla compression + label-based flexibility. The Expert pattern: match the TSDB architecture to the workload — Prometheus/VictoriaMetrics for observability (label-based, PromQL, Grafana); TimescaleDB for SQL-native time-series with dimensional joins (hypertables, continuous aggregates); InfluxDB 3.0 or ClickHouse for petabyte-scale analytical time-series (columnar, Iceberg/Parquet). Understanding when each fits, and how to compose them in real infrastructure, is the specific engineering competence for modern data platforms. §02 covers time-series storage internals (WAL + chunks + Gorilla). §03 covers the three architectures and their systems. §04 lets you explore all three modes across three workloads.

The historical arc of time-series databases is specifically the story of increasingly specialized infrastructure for high-frequency append-only workloads. 1999: RRDtool (Tobi Oetiker). Round-robin database; fixed-size ring buffer per metric; automatic aging + downsampling built-in. Standard for Cacti/Munin/Nagios. Ingenious for its constraints (predictable disk usage) but schema-rigid. 2003: Graphite (Chris Davis, Orbitz). Whisper storage format (RRD-inspired); Carbon daemon for ingestion; metric names as dotted paths (`app.server.cpu`). Widely deployed 2005-2015. 2011: OpenTSDB (StumbleUpon). HBase-based; scales further; tag-based metrics; complex ops. 2013: InfluxDB 0.x (InfluxData). Purpose-built TSDB; InfluxQL query language; TSM (Time-Structured Merge) storage; single-binary deployment. Popular for its time. 2015: Prometheus 1.0 (SoundCloud → CNCF). Pull-based scraping; label-based metrics; PromQL query language; simpler ops than InfluxDB; Kubernetes-friendly. Rapid adoption in cloud-native. 2015: Gorilla paper (Facebook / Meta). "Gorilla: A Fast, Scalable, In-Memory Time Series Database." Delta-of-delta timestamps + XOR-encoded floats = 12× compression on typical Facebook infrastructure metrics. 26-byte points → 1.37 bytes/point. Adopted by essentially every modern TSDB. 2017: TimescaleDB 0.1. Postgres extension; hypertables auto-partition by time; SQL-native; brought time-series to teams comfortable with Postgres. 2018: VictoriaMetrics. Prometheus-compatible but 10× more efficient (memory, disk, CPU); single binary; drop-in replacement. Rapid adoption for scale. 2020: InfluxDB 2.0 with Flux. New query language (Flux); UI; task-based automation. Split community reception (many preferred InfluxQL/SQL). 2021-2022: Observability standard. Prometheus + Grafana + Alertmanager becomes the standard cloud-native monitoring stack. Thanos, Cortex, Mimir add multi-tenant + long-term storage over object storage. 2023: InfluxDB 3.0 rewrite. Complete rewrite in Rust on Apache Iceberg + Parquet + DataFusion. Separates storage from compute. Native SQL + InfluxQL. Moves toward analytical convergence. 2024+: TSDB / OLAP distinction blurs. ClickHouse eats into TSDB space (excellent for time-series analytics). InfluxDB 3.0 is essentially an analytical database with time-series optimizations. Meanwhile Prometheus/VictoriaMetrics remain dominant for operational observability (label-based, PromQL). Three-way split: label-based operational TSDB (Prometheus/VictoriaMetrics), SQL-native time-series (TimescaleDB), columnar analytical time-series (InfluxDB 3.0/ClickHouse). The historical arc explains why time-series is its own architectural paradigm — general-purpose OLTP and OLAP both fail at what time-series-specific systems handle natively, and the specific engineering discipline is knowing which architecture fits which workload.

Time-series is neither OLTP nor OLAP. It\'s continuous high-frequency append-only ingestion with time-oriented queries. Purpose-built infrastructure required — Prometheus for observability, Timescale for SQL, ClickHouse for analytical scale.
§ 02 — Time-series storage internals · WAL + chunks + Gorilla compression

Timestamp as
primary axis.
Compress
12×.

Time-series storage engines are specifically optimized around three characteristics: append-only writes at high frequency, timestamp-ordered range queries, and highly compressible sequential data. The standard architecture: (a) writes go to an in-memory memtable + WAL (Write-Ahead Log) for durability; (b) memtable flushes to immutable time-bounded chunks on disk; (c) chunks compact over time (merging older chunks into larger ones for space efficiency); (d) queries read relevant chunks based on time range + labels/filters. This is essentially an LSM-tree (M.53) specialized for time-series: instead of general key-value with SSTables, chunks are time-bounded (all data in chunk X falls within timestamp range [T1, T2]). Gorilla compression (Facebook 2015) exploits the specific structure of time-series data: consecutive timestamps often differ by exact intervals (delta-of-delta = 0); consecutive float64 values often share most bits (XOR encoding compresses to few bits). Together: 12× compression on typical operational metrics. Standard for Prometheus, VictoriaMetrics, InfluxDB, M3DB. Understanding these internals is Expert-tier TSDB engineering competence.

// TIME-SERIES STORAGE ENGINE · WAL + CHUNKS + GORILLA COMPRESSION

TIME-SERIES STORAGE ENGINE · WRITE PATH + COMPRESSION WRITE PATH Sample arrives ↓ WAL append + memtable ↓ every 2h Flush to chunk on disk CHUNKS (IMMUTABLE) chunk[0]: 00:00-02:00 UTC chunk[1]: 02:00-04:00 UTC chunk[2]: 04:00-06:00 UTC ↓ compact daily compacted-day.chunk Gorilla + delta-of-delta QUERY PATH Query: metric[5m] ↓ Locate relevant chunks by ts ↓ Decode Gorilla · aggregate GORILLA COMPRESSION · FACEBOOK 2015 · 12× RATIO ON TYPICAL METRICS Timestamp: delta-of-delta encoding raw: 1700000000, 1700000015, 1700000030, 1700000045, 1700000060 (5 × 8B = 40 B) delta: 15, 15, 15, 15 (typical scrape interval → all same) delta-of-delta: 0, 0, 0 (regular interval → mostly zeros → 1 bit each) → 40 B → ~2 B (20× compression on regular-interval timestamps) Value: XOR-based encoding (float64) consecutive values often near each other → XOR has many leading/trailing zeros → variable-bit encoding → 40 B → ~14 B typical (3× compression on values) · combined = 12× overall
The specific time-series storage engine mechanism. Write path: samples arrive (typically via HTTP scrape for Prometheus or write endpoint for Timescale/Influx); each sample is (metric, labels, timestamp, value); appended to Write-Ahead Log for durability; inserted into in-memory memtable indexed by (labels, timestamp); memtable flushed to immutable chunk on disk periodically (Prometheus every 2 hours; TimescaleDB by hypertable chunk interval; InfluxDB by shard duration). Chunks on disk: each chunk contains all data for a specific time range across all series; internally structured with per-series compressed timestamp+value arrays; chunk index maps series to byte offsets. Chunks are immutable — writes never modify existing chunks. Compaction jobs merge multiple 2-hour chunks into 24-hour chunks over time for space efficiency. Query path: parse query; identify relevant time range; locate chunks intersecting that range (skip everything else — cheap!); for each relevant chunk, look up series matching labels/filters; decode Gorilla-compressed data; aggregate per query. Range queries scan contiguous chunk bytes; extremely cache-friendly. Gorilla compression (Facebook 2015): two orthogonal techniques. (a) TIMESTAMP: delta-of-delta encoding. Most samples arrive at regular intervals (15s Prometheus scrape). Raw timestamps are 8 bytes each; deltas between them are typically constant (15, 15, 15, ...); delta-of-delta is mostly zero (0, 0, 0, ...); zero-runs encode in 1 bit each. Non-zero delta-of-deltas encoded in variable-length: 1 bit flag + 2 bits (0-6 range) or 5 bits (0-32) or 9 bits (0-512) or 12 bits (larger). Typical timestamp encoding: 1-2 bits per sample vs 64 bits raw = 20-40× compression on timestamps. (b) VALUES: XOR-based encoding. Consecutive float64 values in operational metrics (CPU %, request rates) are often close together — their XOR has many leading + trailing zeros. Encoding: XOR with previous value; count leading/trailing zeros; encode only middle "meaningful" bits with variable length. For similar values, 5-15 bits per sample vs 64 bits raw = 4-13× compression. Combined: 12× overall compression on typical operational time-series; some datasets (very regular scrape + slowly-changing values) achieve 30×. Standard for Prometheus, VictoriaMetrics, InfluxDB, M3DB, TimescaleDB\'s time_bucket compression. The critical insight: time-series data has specific statistical structure (regular timestamps + smoothly-varying values) that Gorilla exploits — general-purpose compressors (gzip, LZ4) don\'t achieve nearly this ratio on time-series because they don\'t understand the domain-specific patterns.
i
WAL + memtable.

Write-Ahead Log ensures durability (survives crashes); memtable holds recent samples in memory for fast query + eventual flush. Prometheus: 2-hour blocks. Similar in every modern TSDB. Standard LSM-tree write path adapted for time-series.

ii
Immutable time-bounded chunks.

Each chunk covers a specific time range across all series. Immutable after flush — writes never modify existing chunks. Enables aggressive compression + memory-mapping. Queries skip irrelevant chunks entirely. Foundation of TSDB performance.

iii
Gorilla delta-of-delta timestamps.

Regular-interval timestamps (15s scrape) → delta constant → delta-of-delta zero → 1 bit per timestamp. 20-40× compression. Facebook 2015 paper. Standard in Prometheus, VictoriaMetrics, InfluxDB.

iv
Gorilla XOR value encoding.

Consecutive float64 values close → XOR has leading/trailing zeros → variable-bit encoding of middle bits. 3-13× compression on values. Optimal for CPU/memory/request-rate metrics with smooth variation.

v
Compaction + downsampling.

Older chunks compacted into larger chunks (2h → 24h → 7d). Optionally downsampled during compaction (raw → 1m rollups → 5m → 1h). Automatic retention drops chunks past N days. Storage cost bounded.

vi
Time-based indexing.

Chunks indexed by time range → range queries locate relevant chunks in O(log N). Series indexed by labels within chunk (Prometheus: postings list per label pair). Enables sub-second queries over 30-day windows on billions of samples.

The WAL + memtable + chunk architecture (i+ii) is a specific instantiation of LSM-tree design (M.53) specialized for time-series. Standard LSM handles arbitrary key-value writes with SSTable levels + compaction; time-series LSM specializes on the fact that writes are naturally time-ordered (mostly). Chunks are time-bounded: chunk[0] = 00:00-02:00 UTC, chunk[1] = 02:00-04:00 UTC, etc. This makes several optimizations possible: (a) Chunk pruning — a query for "last 5 minutes" only reads the latest chunk; queries for "last 30 days" read ~360 2-hour chunks (or fewer, if compacted to larger chunks). Massive I/O savings vs OLTP full-table scan. (b) Bounded write amplification — since chunks are time-bounded and immutable, write amplification comes only from compaction. Prometheus flushes memtable every 2 hours; compacts 2h chunks into 24h chunks daily; further compacts into 7-day chunks weekly (in Thanos/Cortex/Mimir long-term storage). Total write amplification ~3-4× vs 10-20× typical for general-purpose LSM. (c) Cache-friendly access — sequential timestamp order means chunks read from disk sequentially; kernel readahead helps; SIMD-accelerated Gorilla decoding processes samples in tight loops. Sub-microsecond per sample decode. (d) Simple concurrency model — writers append to current memtable + WAL; readers read from finished chunks + current memtable (with lock-free snapshot). No complex MVCC needed. Standard modern TSDB engineering. TimescaleDB uses similar hypertable-chunk architecture at the SQL layer (each chunk = a Postgres child table partitioned by time); InfluxDB uses TSM (Time-Structured Merge tree); ClickHouse uses MergeTree with time as sorting key. Different systems, same fundamental architecture. Understanding this is Expert-tier storage engine competence for time-series.

The Gorilla compression breakdown (iii+iv) deserves specific attention because it\'s the specific innovation that made modern TSDBs economically viable at scale. Consider the naive storage cost: 1M active series × 4 samples/minute × 16 bytes/sample (8B timestamp + 8B float64 value) × 30 days × 24h × 60m = 2.76 TB per month of raw metrics for a moderate observability workload. At AWS EBS $0.10/GB/month = $276/month just for storage of one month\'s data; not counting IOPS charges. Gorilla\'s 12× compression → $23/month. Over years of data retention, the difference is enormous. Specifically how Gorilla works: (a) TIMESTAMP encoding. Prometheus scrapes typically at 15-second intervals. Raw timestamps for one series over an hour: 1700000000, 1700000015, 1700000030, ..., 1700003585 (240 timestamps × 8 bytes = 1920 bytes). Compute deltas: 15, 15, 15, ..., 15 (240 × 4 bytes as int32 = 960 bytes). Compute delta-of-deltas: 0, 0, 0, ... (mostly zeros). Encode: zero delta-of-delta = 1 bit (special marker); non-zero uses variable-length coding (1 bit flag + 2/5/9/12 bit value depending on magnitude). Result: ~240 bits = 30 bytes for the hour vs 1920 bytes raw = 64× compression on timestamps for this ideal case. Real workloads (with scrape jitter, occasional missed scrapes) achieve 20-40× typical. (b) VALUE encoding. Consider CPU % values: 12.3, 12.5, 12.4, 13.1, 12.8, ... IEEE 754 double representation puts these close in binary form — most bits match. XOR consecutive values → mostly zero bits with some meaningful bits in the middle. Encoding: (leading zeros count, meaningful bits count, meaningful bits). If consecutive XORs have the same leading/trailing zero counts, share the block info. Result: 5-15 bits per value typical vs 64 bits raw = 4-13× compression. For "counter" metrics (monotonically increasing like request count), even better — deltas are always positive integers with predictable magnitudes; encode with 8-16 bits typically. (c) Combined effect: real Facebook production data compressed 12× via Gorilla. Extreme cases (very regular scrapes on slowly-changing metrics) hit 30×. Standard for every serious modern TSDB. Understanding this specific compression is Expert-tier — because it explains why observability is economically viable at cloud-native scale.

Every purpose-built TSDB uses time-bounded chunks + LSM-tree writes + Gorilla-style compression. The 2015 Facebook paper defined the era. 12× compression is what makes cloud-native observability affordable.
§ 03 — Three architectures · label-based / hypertable SQL / columnar analytical

Prometheus. TimescaleDB.
ClickHouse.
Three shapes,
three fits.

Modern time-series infrastructure has converged on three canonical architectural approaches, each optimized for different workload characteristics. (a) Label-based TSDB (Prometheus, VictoriaMetrics) — metric identified by name + labels {job, instance, method, status}; pull-based scraping; PromQL query language; Grafana visualization; Gorilla compression. Designed for observability where high-cardinality label combinations matter and queries are ad-hoc dashboards over recent time windows. Dominant for cloud-native monitoring. (b) Hypertable SQL (TimescaleDB) — Postgres extension; hypertables automatically partition tables by time; native SQL with time-series superpowers (time_bucket, first, last, lag, LOCF); continuous aggregates for materialized rollups; native columnar compression; SQL joins to dimensional tables. Best when time-series lives alongside relational data and teams want SQL familiarity. Popular for IoT, industrial, financial applications. (c) Columnar analytical (InfluxDB 3.0, ClickHouse) — column-oriented storage; Parquet + Iceberg (InfluxDB 3.0) or native MergeTree (ClickHouse); SQL query language; separates storage from compute; petabyte scale. Blurs into OLAP. Best for large-scale time-series analytics, historical backtesting, cross-cutting analytical queries. Understanding when each fits is Expert-tier data infrastructure competence — because most companies at scale end up composing all three (Prometheus for ops, TimescaleDB for product data, ClickHouse for analytics).

// LABEL-BASED VS HYPERTABLE VS COLUMNAR · WHEN EACH FITS

THREE ARCHITECTURES · SIDE-BY-SIDE LABEL-BASED TSDB Prometheus / VictoriaMetrics DATA MODEL: http_requests_total{job="api", instance="node1", method="GET", status="200"} = 12345 INGESTION: Pull-based · Prometheus scrapes HTTP /metrics endpoints QUERY: PromQL: rate( http_requests_total[5m]) FITS: ✓ Observability + monitoring ✓ Cloud-native / Kubernetes ✓ High-cardinality labels ✗ SQL joins to dimensional data ✗ Complex analytical queries ✗ Petabyte-scale retention HYPERTABLE SQL TimescaleDB DATA MODEL: CREATE TABLE metrics ( ts TIMESTAMPTZ, device_id INT, val FLOAT8) INGESTION: Push-based · INSERT via SQL or COPY / batch load QUERY: SQL + time_bucket: SELECT time_bucket(\'5m\',ts)... FITS: ✓ Time-series + relational join ✓ Full SQL familiarity ✓ Continuous aggregates ✓ IoT, industrial, financial ✗ Pull-model observability ✗ 10M+ writes/sec on 1 node COLUMNAR ANALYTICAL InfluxDB 3.0 / ClickHouse DATA MODEL: Columnar Parquet + Iceberg or ClickHouse MergeTree sorted by (ts, dimensions) INGESTION: Batch (Parquet files) or streaming (Kafka → CH) QUERY: SQL: full analytical DSL + time-series functions FITS: ✓ Petabyte-scale time-series ✓ Analytical backtesting ✓ Financial ticks / event data ✓ Cross-cutting SQL analytics ✗ Real-time dashboards {ms} ✗ Simpler ops than dedicated
The three modern architectures each optimize different aspects of the time-series problem. Label-based TSDB (Prometheus, VictoriaMetrics): pull-based scraping model — Prometheus polls HTTP `/metrics` endpoints of services every 15 seconds; each metric is name + label map (`http_requests_total{job="api", instance="node1", method="GET", status="200"}`); label combinations create "series" (unique combinations = unique series to store); PromQL query language with time-window functions (`rate(http_requests_total[5m])`). Excellent for cloud-native observability where high-cardinality labels matter and queries are ad-hoc dashboards over last few hours. Weakness: joins to dimensional data awkward; SQL familiarity absent; retention/scale requires Thanos/Cortex/Mimir add-ons. Hypertable SQL (TimescaleDB): Postgres extension that transparently partitions tables by time (`SELECT create_hypertable(\'metrics\', \'ts\')`). Full SQL — SELECT time_bucket(\'5 minutes\', ts) AS bucket, device_id, AVG(temperature) FROM metrics WHERE ts > NOW() - INTERVAL \'24 hours\' GROUP BY bucket, device_id. Continuous aggregates materialize rollups automatically. Joins to dimensional tables (devices, locations, users) via standard SQL. Best when time-series alongside relational data (IoT with device metadata, industrial with equipment hierarchy, financial with asset metadata). Weakness: not designed for pull-based observability; scales to millions but not billions of writes/sec on single node (though multi-node available). Columnar analytical (InfluxDB 3.0, ClickHouse): column-oriented storage with time as sorting key. InfluxDB 3.0 rewrote entirely on Apache Iceberg + Parquet + DataFusion; separates storage from compute; native SQL + InfluxQL. ClickHouse: analytical database with time-series-optimized MergeTree; extreme write throughput (10M+/sec); SQL with time-series extensions. Both excellent for petabyte-scale analytical time-series (historical financial data, event analytics, log analytics). Weakness: sub-second real-time dashboards possible but not the primary use case; more complex ops than Prometheus for simple monitoring. Choosing among the three: MONITORING/OBS → Prometheus/VictoriaMetrics. IoT/INDUSTRIAL/APP TELEMETRY WITH DIMENSIONAL DATA → TimescaleDB. LARGE-SCALE ANALYTICS/BACKTESTING → InfluxDB 3.0/ClickHouse. Most companies at scale end up with all three (composed via streaming pipelines).
i
Prometheus + VictoriaMetrics.

Pull-based; label-based; PromQL; single-binary ops; Gorilla compression. Dominant cloud-native observability. VictoriaMetrics: Prometheus-compatible but 10× more efficient (memory, disk, CPU). Standard for Kubernetes monitoring.

ii
Thanos / Cortex / Mimir.

Multi-tenant Prometheus over object storage (S3/GCS/Azure Blob). Long-term retention (years); global query view across regions; horizontal scale. Standard for enterprise observability at scale. Mimir (Grafana Labs) most modern.

iii
TimescaleDB.

Postgres extension; hypertables auto-partition by time; continuous aggregates for materialized rollups; native columnar compression (10× vs uncompressed); retention policies; full SQL + time-series functions (time_bucket, LOCF, first/last). Popular for IoT + industrial + finance.

iv
InfluxDB 3.0.

Complete rewrite (Rust, Apache Iceberg + Parquet + DataFusion). Separates storage/compute. SQL + InfluxQL. Petabyte scale. Analytical convergence. Different beast from InfluxDB 1.x/2.x — plan for migration if upgrading.

v
ClickHouse for time-series.

Analytical column store with time-series superpowers. MergeTree engine sorted by (ts, dims). Materialized views for continuous aggregation. Extreme scale (Cloudflare: 100+ TB/day; Uber: trillions of rows). Standard for large-scale time-series analytics.

vi
PromQL / Flux / SQL.

Query language matters. PromQL: elegant for observability (rate, histogram_quantile, sum by (label)). Flux: functional, powerful, harder to learn. SQL: universal but time-series requires extensions (time_bucket, lag, LOCF). Standard time-series function suite.

The Prometheus + VictoriaMetrics architecture (mech items i+ii) is the dominant cloud-native observability stack. Specifically: (a) Pull-based scraping — Prometheus polls services at their HTTP `/metrics` endpoint every 15s (configurable per scrape job). Services expose metrics via client libraries (prom-client for Node, prometheus_client for Python, micrometer for Java, etc.) that expose them in a simple text format. Advantage: service discovery via Kubernetes API / Consul / DNS; healthchecks are implicit (target down = no scrape). Disadvantage: not ideal for short-lived jobs (may miss scrapes); requires push gateway for batch jobs. (b) Label-based metric identification — every unique combination of labels creates a unique series. `http_requests_total{job="api", instance="node1", method="GET", status="200"}` is one series; changing status to 500 creates another. Powerful for filtering and grouping. Downside: high cardinality creates memory pressure (each active series takes memory for chunk buffering, index entries, etc.). Prometheus stores ~500 bytes-1KB per active series in memory. 1M active series ≈ 500MB-1GB memory. Cardinality explosion is anti-pattern §05.ii. (c) Local storage + limited retention — Prometheus stores locally (typically SSD); retention typically 15-90 days. For longer retention → Thanos/Cortex/Mimir (which offload old chunks to object storage). (d) Grafana as visualization — dashboards over PromQL; alerting via Alertmanager. Ubiquitous. (e) VictoriaMetrics as Prometheus-compatible alternative — same PromQL, same scraping model, but 10× more efficient (Cyrill Zaslavsky\'s benchmarks; independent confirmations). Single-binary vs Prometheus + Thanos complexity. Increasingly the choice for teams optimizing for cost/simplicity. Standard modern observability. Understanding this stack is Expert-tier operational competence for cloud-native infrastructure.

The TimescaleDB architecture (mech item iii) is best when time-series data lives alongside relational data. Specifically: (a) Hypertables: `SELECT create_hypertable(\'metrics\', by_range(\'ts\', INTERVAL \'1 day\'))` — transparently splits the table into per-day child tables (chunks); Postgres query planner routes queries to relevant chunks; INSERTs go to the current chunk. Older chunks can be moved to different tablespaces (e.g., cheap S3-backed storage). (b) Continuous aggregates: `CREATE MATERIALIZED VIEW metrics_5m WITH (timescaledb.continuous) AS SELECT time_bucket(\'5 minutes\', ts) AS bucket, device_id, AVG(temperature) FROM metrics GROUP BY bucket, device_id`. TimescaleDB automatically refreshes this incrementally as new data arrives. Queries against the view are instant (materialized); queries against raw hit only recent unmaterialized rows. Massive dashboard speedup. (c) Native compression: older chunks can be compressed via columnar storage (`ALTER TABLE metrics SET (timescaledb.compress, ...) SELECT add_compression_policy(\'metrics\', INTERVAL \'7 days\')`). 10× compression ratio typical. Trade-off: compressed chunks are read-optimized (append still fine; UPDATE/DELETE require decompression). (d) Retention policies: `SELECT add_retention_policy(\'metrics\', INTERVAL \'90 days\')` — TimescaleDB automatically drops chunks older than 90 days. (e) Full SQL + time-series functions: `time_bucket()` (window into time buckets), `first()/last()` (first/last value in group), `locf()` (last observation carried forward for gap-filling), `time_weight()` for averages over irregular series. Standard time-series function library. (f) Joins to dimensional tables: standard SQL joins between hypertable and regular Postgres tables. Devices table with metadata (location, owner, model); metrics hypertable with values; JOIN gives you full analytical context. This is the killer feature for IoT and industrial applications. Understanding TimescaleDB is Expert-tier for Postgres-native time-series workloads.

The columnar analytical architecture (mech items iv+v) — InfluxDB 3.0 and ClickHouse — represents the convergence of TSDB and OLAP. Specifically: (a) InfluxDB 3.0: complete rewrite (2023) in Rust on Apache Iceberg + Parquet + DataFusion. Storage is Parquet files in object storage (S3, GCS); Iceberg for table management; DataFusion for query execution (Rust-based analytical engine). Separates storage from compute (like Snowflake). SQL as primary query language (InfluxQL still supported). Petabyte scale. Very different from InfluxDB 1.x (TSM engine) and 2.x (Flux) — migration required. Represents the "TSDB as analytical database" trend. (b) ClickHouse: originally OLAP (Yandex web analytics); adapted excellently for time-series. MergeTree storage engine: sorted by primary key (typically (ts, dimensions)); LSM-like merging; columnar compression (LZ4, ZSTD, and time-series-specific like Gorilla). Extreme write throughput — Cloudflare ingests 100+ TB/day into ClickHouse; Uber has trillion-row tables. Materialized views for continuous aggregation (similar to TimescaleDB continuous aggregates). SQL with time-series-friendly extensions. Popular for large-scale event data, logs, financial ticks, session analytics. (c) When columnar analytical fits: large-scale analytics workloads (backtesting, historical analysis, cross-cutting queries); when you already have data lake infrastructure (S3 + Parquet); when analytical SQL matters more than sub-millisecond dashboard latency. Not as tight for simple monitoring dashboards as Prometheus. Complementary to Prometheus/TimescaleDB, not a replacement. Understanding when to use columnar analytical is Expert-tier data architecture competence — the convergence of TSDB and OLAP is a defining trend of 2023-2025 data infrastructure.

Prometheus for the ops screens. TimescaleDB for the SQL joins to dimensional data. ClickHouse for the petabyte-scale historical analytics. Every mature company composes all three.
§ 04 — Time-series explorer

Three architectures.
Three workload types.

Below: each of three time-series architectures (Label-based TSDB (Prometheus/VictoriaMetrics) · Hypertable SQL (TimescaleDB) · Columnar analytical (InfluxDB 3.0 / ClickHouse)) evaluated against three workload types (Application metrics · IoT sensor data · Financial market data). Watch how each architecture fits or fails each workload — the diagonals reveal where each architecture dominates its native workload, and the off-diagonals show where the wrong choice produces measurably worse cost, latency, or operational complexity. The takeaway: match architecture to workload characteristics; composite architectures (all three) are common at scale.

TSDB.SIM // m.60 lab
Workload →
// STORAGE BEHAVIOR · under current workload
// METRICS · INGESTION / QUERY / STORAGE / OPS PROFILE
Write throughput-
Query latency p95-
Compression-
Retention scale-
Ops complexity-
Overall fit-
// VERDICT
Loading...
...
§ 05 — Where time-series decays

Every regret is
a cardinality,
a wrong tool,
or no retention.

The failure modes of time-series infrastructure are the specific mechanisms by which "our monitoring is slow" turns into "our storage bill is $50K/month" or "Prometheus OOM-killed at 3AM again." Each of these anti-patterns is a real production pattern; Expert engineers avoid them by matching the architecture to workload, controlling cardinality explicitly, configuring retention + downsampling policies, and understanding when purpose-built TSDBs are the right tool. Recognizing these saves months of "why is our time-series infrastructure broken" debugging.

// FIVE TIME-SERIES ANTI-PATTERNS

i
The time-series in vanilla Postgres OLTP
"We store metrics in Postgres. It worked fine at 1K writes/sec. Now at 50K/sec we\'re seeing WAL saturation, autovacuum falling behind, dashboard queries taking 30+ seconds, and disk usage growing 100GB/day."

Vanilla Postgres OLTP with a `(metric_name, timestamp, value)` table fails at time-series-scale writes due to specific mechanisms: (a) B-TREE index write amplification — every INSERT triggers B-tree page rewrites; index maintenance dominates I/O. (b) WAL saturation — 50K writes/sec × ~200 bytes WAL record = 10MB/sec WAL; combined with autovacuum WAL activity, exceeds disk bandwidth. (c) Table bloat + slow queries — 50K/sec × 86400s = 4.3B rows/day; without partitioning, index scans degrade; sequential scans of billion-row tables take minutes. (d) No native downsampling or retention — data grows unbounded. The fix: (i) INSTALL TIMESCALEDB — same Postgres, add extension, `SELECT create_hypertable(\'metrics\', \'ts\')`. Automatic time-based partitioning. Continuous aggregates for materialized rollups. Native compression (10× vs uncompressed). Retention policies. Handles millions of writes/sec on modest hardware. Standard Postgres-native fix. Common migration path from vanilla Postgres. (ii) OR MIGRATE TO PROMETHEUS/VICTORIAMETRICS for pull-based observability workloads. (iii) OR CLICKHOUSE for analytical time-series at extreme scale. Choice depends on workload: SQL joins to dimensional data → Timescale; observability → Prometheus/VM; analytical → ClickHouse. The general principle: vanilla OLTP databases fail at time-series-scale continuous ingestion; purpose-built TSDBs (or Postgres extensions like Timescale) are required. Anti-pattern §05.i.

ii
The high-cardinality label explosion
"Our Prometheus keeps getting OOM-killed. We just added a `user_id` label to our metrics to track per-user request rates. Suddenly we have 5 million active series and Prometheus is using 40GB of RAM."

Cardinality is the number of unique label-value combinations creating unique series. Each active series consumes ~500 bytes-1KB in Prometheus memory (chunk buffer + index entries). Labels with high cardinality (user_id, session_id, request_id, URL path with variable parts) cause series count explosion. 5M active series × 700 bytes = 3.5GB series metadata alone; plus chunk buffers ≈ 10-40GB memory. OOM at any scale ceiling. Query performance also suffers (must aggregate over millions of series). The fix: (a) REMOVE HIGH-CARDINALITY LABELS from Prometheus. User IDs, session IDs, request IDs, unbounded IDs should NEVER be Prometheus labels. Instead: aggregate to lower cardinality (`method` + `status` + `endpoint_pattern` not `user_id`). (b) FOR PER-USER ANALYTICS — use a different tool. TimescaleDB with user_id as a regular column handles this (Postgres cardinality has no series-metadata cost). ClickHouse similarly. Prometheus is wrong tool. (c) USE HISTOGRAMS INSTEAD OF LABELS — for latency distributions, use histogram buckets not per-user latency labels. (d) MEASURE cardinality via Prometheus\'s own metrics — `prometheus_tsdb_head_series` shows active series count; alert on explosive growth. (e) CONSIDER VICTORIAMETRICS — handles higher cardinality (~10× vs Prometheus) but same principle applies. The general principle: label-based TSDBs have specific cardinality limits; high-cardinality attributes belong in different storage (SQL, ClickHouse); avoiding cardinality explosion is Expert-tier Prometheus discipline. Anti-pattern §05.ii.

iii
The no retention · no downsampling
"Our Prometheus has 2 years of raw 15-second data. Storage is 8TB. Dashboards showing last-month views take 60+ seconds. We spent $80K on faster SSDs and it barely helped."

Time-series data without retention + downsampling grows unbounded and destroys query performance for long-range queries. 15-second scrape × 2 years = 4.2M samples per series × 1M active series = 4.2 trillion samples. Even at 1.3 bytes/sample (Gorilla), that\'s 5.4TB of raw data. Queries over "last 30 days" must scan billions of samples; even with Gorilla decoding at 1GB/s, that\'s tens of seconds. Fundamentally wrong to store raw high-frequency data long-term. The fix: (a) CONFIGURE RETENTION — Prometheus: `--storage.tsdb.retention.time=30d`. Keep only 30 days locally. VictoriaMetrics: similar. TimescaleDB: `SELECT add_retention_policy(\'metrics\', INTERVAL \'30 days\')`. (b) DOWNSAMPLE FOR LONG-TERM — Prometheus recording rules: `record: http_requests:rate5m expr: rate(http_requests_total[5m])` — computed every scrape; stored as new series. Query recording rule instead of raw metric for dashboards. (c) THANOS/CORTEX/MIMIR — offload old data to S3; automatic downsampling to 5m + 1h resolution; long-term retention (years) cheap; global query view. (d) TIMESCALEDB CONTINUOUS AGGREGATES — materialized rollups; query the rollup for dashboards instead of raw. (e) MEASURE — track query latency by time range; identify slow long-range queries; move them to rollup views. The general principle: raw high-frequency data is for recent queries (last few hours-days); older data queried via rollups; retention drops truly old data. Standard TSDB discipline. Anti-pattern §05.iii.

iv
The OLAP warehouse for high-frequency ingestion
"We\'re piping 500K metrics/sec into Snowflake. The bill is $200K/month. Queries are 30+ seconds. Somebody said BigQuery would be cheaper but it\'s the same story."

OLAP warehouses (Snowflake, BigQuery, Redshift) are optimized for large analytical queries over batch-loaded data, not continuous high-frequency ingestion. Streaming ingestion costs are per-slot or per-warehouse-hour + storage; 500K writes/sec × 24h × 30d = ~1.3B rows/day; ingestion + storage + query compute adds up. Also: query latency is second-to-minute scale (not sub-second like TSDBs); no time-series-native functions (must roll your own downsampling); overkill for observability workloads. The fix: (a) FOR OBSERVABILITY/MONITORING — Prometheus/VictoriaMetrics + Thanos/Cortex/Mimir. Same infrastructure runs at 10-100× less cost. (b) FOR SQL-NATIVE + JOINS — TimescaleDB. Postgres extension; native SQL; excellent compression + continuous aggregates. Much cheaper than Snowflake for continuous ingestion. (c) FOR ANALYTICAL TIME-SERIES — ClickHouse or InfluxDB 3.0. Purpose-built for this; often 10-50× cheaper than Snowflake for time-series analytical workloads while providing better latency. (d) ARCHITECTURE — keep hot data (last 90d) in purpose-built TSDB; cold data (90d+) can go to Snowflake/BigQuery for occasional analytical queries via CDC. Composite architecture. Standard modern data platform pattern. (e) MEASURE — track Snowflake credits per query; identify time-series workload cost drivers; migrate them to purpose-built systems. The general principle: OLAP warehouses have specific fits (batch analytics, complex joins over dimensional data); continuous high-frequency time-series is not one of them. Purpose-built TSDBs deliver 10-100× cost reduction for observability + analytical time-series. Anti-pattern §05.iv.

v
The ignoring Gorilla / time-series compression
"We\'re rolling our own time-series store because we need custom features. Storage cost is 40TB/month. Our metrics are floats; we\'re storing them as 8-byte doubles + 8-byte timestamps + JSON labels."

Time-series data compressed naively (raw floats + timestamps) is 10-30× larger than Gorilla-compressed. This translates directly to storage cost + query I/O latency. Fundamental time-series-specific insight: consecutive timestamps are highly regular (delta-of-delta = 0); consecutive values in operational metrics vary smoothly (XOR has many zero bits). General-purpose compression (gzip, zstd) achieves 2-4× on time-series; Gorilla achieves 12×. The difference: Gorilla understands the domain-specific structure. The fix: (a) DON\'T ROLL YOUR OWN — use Prometheus, VictoriaMetrics, TimescaleDB, InfluxDB, or ClickHouse. All implement Gorilla or equivalent. If features are missing, contribute upstream (open-source) rather than reimplement. (b) IF YOU MUST — implement delta-of-delta timestamps (well-documented in Gorilla paper); implement XOR value encoding; test compression ratio on realistic data. Don\'t use JSON labels — interned strings or dictionary encoding much cheaper. (c) MEASURE — compression ratio per metric type; identify high-cardinality/high-volume metrics for optimization focus. (d) CONSIDER ADDITIONAL COMPRESSION — ZSTD or LZ4 on top of Gorilla for further 20-40% reduction (at CPU cost). Standard for cold storage tiers. (e) COLUMNAR — for analytical workloads, columnar storage (Parquet) with time-series-specific encoding (dictionary, delta, RLE) beats row-based. Standard for InfluxDB 3.0, ClickHouse. The general principle: time-series has specific statistical structure (regular timestamps, smooth values) that domain-specific compression exploits; rolling your own means paying 12× more for storage; use purpose-built systems. Anti-pattern §05.v.

The composite pattern across all five is that time-series failure modes reflect specific engineering understanding gaps that Expert-tier competence addresses. Vanilla Postgres fails because it wasn\'t designed for continuous append-heavy writes with time-range queries. High-cardinality labels break Prometheus because each series has per-series metadata cost. Missing retention/downsampling explodes storage and destroys query performance. OLAP warehouses are wrong tool for high-frequency ingestion — 10-100× cost premium for the same workload. Rolling your own storage without Gorilla means paying 12× more for storage than necessary. Each has a specific fix: (a) purpose-built TSDB or Postgres extension (TimescaleDB) for high write throughput; (b) label discipline + moving high-cardinality data to different storage; (c) retention + downsampling policies + Thanos/Cortex/Mimir for long-term; (d) purpose-built TSDBs instead of OLAP for continuous ingestion; (e) using existing systems with Gorilla compression rather than reimplementing. Getting time-series architecture right is the specific engineering discipline that turns "our observability costs are exploding" into "we have year-of-history queries in 200ms at 1/10 the cost."

Every time-series regret is a wrong tool, a cardinality bomb, or missing retention. Prometheus is for observability. Timescale is for SQL. ClickHouse is for analytics. Match architecture to workload. Standard modern discipline.
§ 06 — Eight words for the time-series conversation

Vocabulary,
for the temporal case.

The terms that show up in every observability discussion, every TSDB evaluation, every time-series capacity plan.

Time-Series Database
/taɪm ˈsɪəriːz ˈdeɪtəbeɪs/
Database optimized for continuous high-frequency append-only writes with timestamp-oriented range queries. Uses time-bounded immutable chunks, Gorilla-style compression, native downsampling + retention. Three canonical architectures: label-based (Prometheus), hypertable SQL (TimescaleDB), columnar analytical (ClickHouse/InfluxDB 3.0).
WAL
/wɔːl/
Write-Ahead Log — durable append-only record of all writes before they hit main storage. Enables crash recovery. Standard in TSDBs (Prometheus WAL flushed every 2h into chunks) as in other LSM-tree systems.
Downsampling
/ˈdaʊnˌsæmplɪŋ/
Aggregating high-frequency raw data into lower-frequency rollups (raw 15s → 1m avg → 5m avg → 1h avg → 1d avg). Enables fast long-range queries + bounded storage cost. Prometheus recording rules, TimescaleDB continuous aggregates, Thanos/Cortex compaction.
Retention Policy
/rɪˈtɛnʃən ˈpɒlɪsi/
Rule that automatically drops data older than N days/months/years. Essential for bounded cost + query performance. Prometheus: `--storage.tsdb.retention.time`. TimescaleDB: `add_retention_policy()`. Standard TSDB hygiene.
Gorilla Compression
/ɡəˈrɪlə kəmˈprɛʃən/
Time-series-specific compression: delta-of-delta timestamps + XOR-encoded values. Facebook 2015. 12× compression on typical operational metrics. Standard in Prometheus, VictoriaMetrics, InfluxDB, M3DB. Makes cloud-native observability economically viable.
Cardinality
/ˌkɑːrdɪˈnælɪti/
Number of unique label-value combinations (unique time series). In Prometheus each series costs ~500B-1KB memory. High cardinality (user_id, request_id as labels) causes memory blowup + query slowdown. Standard Prometheus operational concern.
Continuous Aggregate
/kənˈtɪnjuəs ˈæɡrɪɡət/
Materialized view of time-bucketed aggregations, refreshed incrementally as new data arrives. TimescaleDB flagship feature. Massive dashboard speedup — query pre-computed rollup instead of scanning raw. Similar to Prometheus recording rules but SQL-native.
PromQL
/prɒm-kjuː-ɛl/
Prometheus Query Language — functional DSL for label-based time-series with time-window operations. `rate(http_requests_total[5m])`, `sum by (job) (rate(...))`, `histogram_quantile(0.99, ...)`. Standard for cloud-native observability. Grafana dashboards, Alertmanager rules.
§ 07 — Knowledge check

Five questions.
The temporal intuition.

Test the time-series understanding. Click an answer; explanation drops in instantly.

QUESTION 1 OF 5
Loading question...
Score: 0 / 5
5 / 5

Time earned.

Perfect. Time-series storage internals, Gorilla compression, label-based / hypertable / columnar architectures — the specific engineering discipline. Next: M.61.

§ 08 — The recap

Three ideas to
carry forward.

The composite understanding that turns "our metrics are slow and expensive" into "we chose Prometheus for observability, TimescaleDB for IoT with joins, ClickHouse for analytical scale — with retention + downsampling from day one."

i

Time-series is its own paradigm

Not OLTP (fails at continuous high-frequency writes; B-tree contention, WAL saturation, no downsampling). Not OLAP (10-100× cost premium for time-series ingestion; second-scale query latency). Purpose-built TSDBs use time-bounded chunks + Gorilla compression + retention/downsampling. Standard modern data infrastructure.

ii

Match architecture to workload

Label-based (Prometheus/VictoriaMetrics) for cloud-native observability — PromQL, Grafana, Kubernetes-friendly. Hypertable SQL (TimescaleDB) for IoT/industrial/financial with dimensional joins — Postgres extension, continuous aggregates. Columnar (InfluxDB 3.0/ClickHouse) for petabyte analytical time-series. Each optimizes different aspects.

iii

Retention + downsampling from day one

Raw high-frequency data for last N days; downsampled rollups for historical queries; automatic retention drops truly old data. Bounded cost + fast queries. Configure from day one; retrofitting is painful. Prometheus recording rules, TimescaleDB continuous aggregates, Thanos/Cortex/Mimir for object storage. Standard TSDB discipline.

↓ UP NEXT · PHASE J CONTINUES

M.61 — Streaming
architectures.

The next Expert module. Beyond stored time-series and analytical batch — streaming systems (Kafka, Kinesis, Pulsar, Redpanda) handle continuous data flow with sub-second latency. Combined with Flink, Kafka Streams, Materialize for stream processing. The foundation for real-time applications.

Continue to Module 61 →