Lesson 02 of 10 · Modules
Why Prometheus doesn't scale
Why one Prometheus hits a wall: how the TSDB stores data (head block, WAL, 2-hour blocks, compaction), why memory follows active series, how cardinality multiplies, the limits of local retention and single-cluster views, and how to find and stop a cardinality explosion.
Inside the TSDB
scrape ──► head block (memory, last ~2h) ──► WAL on disk (crash recovery)
│ every 2h
▼
immutable 2h block on disk (chunks + index + tombstones)
│ compaction
▼
larger blocks (up to 10% of retention, max 31 days) ─► deleted after retention
- New samples go to the in-memory head block, protected by a write-ahead log (WAL).
- Every 2 hours, the head is cut into an immutable block; compaction merges blocks into larger ones.
- Data is compressed very well (on the order of 1–2 bytes per sample on disk).
- Memory is dominated by the head: every active series costs index entries and recent chunks (roughly a few KB each, varying by version and label sizes). Heavy queries add more.
Prometheus keeps a notebook per patient per measurement. One notebook per patient's temperature is fine. But if you start a new notebook for every visitor who ever came to see the patient (a label with visitor names), the nurse's desk is buried in notebooks and she can't work. That's a cardinality explosion.
Cardinality multiplies
A series is one unique combination of metric name + label values. Series count = product of each label's distinct values:
| Labels | Series |
|---|---|
method (5) × status (10) |
50 |
+ endpoint (50) |
2,500 |
+ pod (40) |
100,000 |
+ user_id (unbounded) |
💥 |
Common culprits: user or session IDs, full URL paths with IDs (/orders/12345), pod IPs, error messages as labels, and histogram buckets multiplied by many labels.
The walls of a single Prometheus
- Memory and CPU: one process, vertical scaling only. Beyond a few million active series, operations get painful.
- Retention: local disk; long retention = big disks, slow long-range queries, and data lost with the volume.
- Scope: one Prometheus per cluster sees only that cluster; there's no global query.
- HA: two replicas scrape the same targets and store two slightly different copies; nothing deduplicates them.
Scenario: the cardinality explosion
Scenario: one label that took down monitoring
A team added a path label with the full request URL to their HTTP metrics. Active series went from 800k to 6 million in an hour. Prometheus was OOM-killed, restarted, replayed its WAL (slowly, and OOM again), and alerting was down for the whole cluster.
How do you recover and prevent it?
Recover
- Stop the source: drop the label or metric at scrape time with
metric_relabel_configs(in the ServiceMonitor'smetricRelabelings), or scale the offending app's scraping down temporarily. - Give Prometheus enough memory to replay the WAL, or (if acceptable) remove the WAL to restart fresh, losing the last couple of hours.
endpoints:
- port: metrics
metricRelabelings:
- action: labeldrop
regex: path
Prevent
sampleLimitper ServiceMonitor (the scrape fails, rather than Prometheus) andenforcedSampleLimitglobally;labelLimit,labelValueLengthLimittoo.- Alert on growth:
prometheus_tsdb_head_seriesrising fast, per-jobscrape_samples_scraped. - Instrumentation review: use route templates (
/orders/{id}), never raw paths or IDs. - Split big tenants into separate Prometheus instances or move to a horizontally scalable backend (lesson 07).
Try it: measure and break (lab only)
- In your kube-prometheus-stack lab, open Status → TSDB Status and note the top metrics by series.
- Run
topk(10, count by (__name__) ({__name__=~".+"}))and compare. - Deploy a small app that exposes a counter with a random
user_idlabel on each request (a few lines of Python withprometheus_client), and generate traffic; watchprometheus_tsdb_head_seriesclimb. - Add a
labeldropmetric relabeling and watch series stop growing (old series age out of the head). - Set
sampleLimit: 1000on the ServiceMonitor and read the scrape error on the Targets page.
Going deeper: sizing a single Prometheus
- Budget memory from measured series counts on your version; test with realistic label sizes.
- Use recording rules to precompute expensive aggregations used by dashboards and alerts.
- Native histograms (a newer feature) can reduce series counts for latency metrics compared to classic bucket histograms; check support in your Prometheus version and clients.
- Scrape interval matters less than series count: 15s vs 30s doubles samples, but not series memory.
Recap
- TSDB: head block in memory + WAL, 2h blocks, compaction; memory tracks active series.
- Cardinality multiplies across labels; unbounded labels explode it.
- Single Prometheus limits: vertical scaling, local retention, no global view, no HA dedup.
- Defend with relabeling, sample limits, growth alerts and instrumentation standards.
This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.