Definition · Time series and cardinalityA time series is identified by a metric name together with a complete set of label values. The cardinality of a label is the number of distinct values it takes. For a metric with labels of cardinality c₁, …, c_m, the number of series is at most
∏ cᵢ = c₁ × c₂ × … × c_m,
reached when every combination occurs. Real systems reach some fraction of it, and the product is the number to plan against.
service 40
endpoint 120
status 6
instance 30
method 4
40 × 120 × 6 × 30 × 4 = 3 456 000 series
scrape interval I = 15 s → 86 400/15 = 5 760 samples/day/series
bytes per sample = 2
3 456 000 × 5 760 × 2 = 39 813 120 000 bytes
= 39.81 GB per day
≈ 14.5 TB per year
Nothing in that list is unreasonable on its own. Forty services is a medium company, a hundred and twenty endpoints is one large service, six status classes is fewer than HTTP defines, thirty instances is a small fleet and four methods is restrained. The product of five reasonable numbers is three and a half million series and forty gigabytes a day, for one metric family.
The label that ends the discussion
Now add a sixth label. A region and zone pair at fifty combinations multiplies everything by fifty: 172.8 million series and about 2 TB a day, which is a large and survivable number. A customer label at five hundred multiplies by five hundred. A user_id label at ten thousand multiplies by ten thousand.
base 3 456 000 series 39.81 GB/day
× region-zone (50) 172 800 000 1.99 TB/day
× customer (500) 1 728 000 000 19.91 TB/day
× user_id (10 000) 34 560 000 000 398.13 TB/day
× request_id (10⁶) 3.456 × 10¹² 39 813 TB/day
The last row is the mistake that gets made, and it is worth being precise about why it is qualitatively different rather than merely larger. A request_id is unbounded: its cardinality is not a hundred thousand or a million, it is however many requests you serve, growing without limit, and every one of those series is written exactly once and then kept for the retention period. A metric with an unbounded label is not an expensive metric; it is a log with the worst possible storage engine.
Example · A URL path as a labelendpoint="/users/12345/orders" looks like one label with a sensible name, and its cardinality is the number of users. The same metric with endpoint="/users/{id}/orders" has cardinality one for that route. The difference between the two is a templating step in the instrumentation, and it is the difference between a hundred and twenty series and ten million.
The tell is that the cardinality of a label should be predictable from the design of the system rather than from its traffic. If you cannot state a label’s cardinality without querying production, it is probably unbounded.
Where the pain actually arrives
The bytes on disk are the easy part; the index is not. Every series needs an entry mapping its label set to its data, that index is consulted on every query and is usually held in memory, and a query that touches a million series has to merge a million streams before it can sum them. A dashboard panel that was fast at three thousand series is not slow at three million — it times out.
This is also why histogram resolution and cardinality are the same budget. Every bucket boundary is a label value, so the choice made in “Histograms and Bucket Error” — a ratio of 2 against a ratio of 5/4, at 3.1 times as many buckets — multiplies this product by 3.1 as well. Precision in the tail and the number of labels you can afford are drawn from one account.
What to do instead
- Bucket the label. An http_status of 2xx, 3xx, 4xx, 5xx has cardinality four; the exact code has cardinality sixty and answers a question you ask twice a year.
- Move the detail to a different system. High-cardinality identifiers belong in logs and traces, which are built to be searched by one key and not aggregated across all of them.
- Template anything derived from a path or a query. The route, not the URL.
- Bound what you cannot template. A label with an explicit allow-list and an other value has the cardinality of the list plus one, for ever, whatever production does.
And compute the product before shipping the label, not after. The multiplication takes ten seconds and the alternative is discovering the answer from an out-of-memory kill on the storage layer at a time of somebody else’s choosing.