Series identity and the cardinality wall
Why does one new label bring down a metrics store when data volume barely changes?
The previous four families were all about a broken predicate. Time series has no broken predicate at all. Time is totally ordered, queries are range scans over it, and a B-tree or a partitioned table handles that perfectly. By the criterion that organizes this mechanism, time series does not belong here.
It belongs because something else is extreme: the data is almost entirely redundant, and this is known at write time. A temperature sensor emits 21.4, 21.4, 21.5, 21.4 every ten seconds, forever. Timestamps arrive at near-constant intervals. Values drift slowly inside a narrow band. Each new point carries a fraction of a bit of real information, wrapped in sixteen bytes of representation (8 bytes for the timestamp and 8 for the value).
That changes which constraint governs. In the other families, the question is how to find data cheaply. Here everything is easy to find and there is simply an enormous amount of it, most of it carrying almost no information. The governing question is how few bytes a point can occupy, and how few points you can afford to keep. Every mechanism in this article is one of those two.
There is a second structural fact, and it is the one that actually breaks systems. Elsewhere the volume problem is rows. Here it is series: the number of distinct identities you are tracking, which is the product of the cardinalities of all the labels you attached. Rows are cheap and compress beautifully. Series are expensive and do not.
A time series is identified by a name plus a set of labels (key-value tags such as city="Pune"). Cardinality means the number of distinct values. Each distinct label combination is a separate series, with its own storage, its own index entry, its own in-memory write buffer, and its own open chunk.
The arithmetic is multiplicative: the number of series is the product of the label cardinalities, so adding a dimension multiplies while adding rows only adds, and one unbounded label (a user id, a request id) turns a bounded system into an unbounded one. Why labels multiply and which signal an unbounded dimension belongs in is observability's question; what matters for the store is the next fact.
It is fatal rather than merely expensive because per-series overhead is not compressible. Each series needs a stored label set, an index entry that makes it findable, and a small active write buffer held in memory. That overhead is paid per series, whatever the number of points. A million series with one point each cost far more than one series with a million points, even though the data volume is identical.
Haatly's case (numbers are an illustration). order_queue_depth had labels city (12 values) and status (5 values), giving 12 × 5 = 60 series. Adding user_id with 2,000,000 values gives 60 × 2,000,000 = 120,000,000 series. If each costs about 3 KB of memory for its label set, index entry and write buffer, that is 120,000,000 × 3 KB = 360 GB. The data rate has not changed, but the memory bill is 2,000,000 times higher (360 GB against 60 × 3 KB = 180 KB).
This has a general form, and it is the same shape as several failures already seen: cardinality is a write-path cost that looks like a read-path convenience. The person adding the label buys query flexibility. The cost lands on the ingestion path, on memory, and on everyone else sharing the system. The cost has not gone away; it has moved, with an unusually clear mismatch between who decides and who pays. It is the organizational version of the rule rather than its physical one.
It also explains why the standard resolution is a quota enforced at write time: limit the labels, reject high-cardinality ones, drop series above a threshold. This is admission control (refusing work at the door when the system is full) applied to schema rather than to queries. It is the only mechanism that works, because by the time cardinality shows up in a query, it has already been paid for.
One line: rows add, labels multiply. Per-series overhead is incompressible, so cardinality is the binding constraint, not volume.