The system that tells you everything else is broken has an unusual requirement: it has to
be the last thing standing. It also has an unusual workload, because the writes never stop
and never slow down. Every server, every second, forever, whether anyone is looking or not.
Metrics system / at a glance
The thing that kills these systems is not volume. It is cardinality, and it kills them
suddenly, from a one line code change nobody reviewed carefully.
Two of the answers below are the ones that matter, and neither is about how many metrics
a second you can take. Find them.
You ask
They say
What it settles
How many machines, and how many metrics does each expose?
A hundred thousand hosts, a few hundred series each.
Tens of millions of active series, which is a storage engine decision. It is also the number that a single careless label can multiply.
At what interval, and does it ever stop?
Every ten seconds, forever, whether anyone is watching or not.
A constant write rate with no idle period. There is no quiet hour to run compaction in, so background work has to survive alongside full load.
Who controls what gets labelled?
Every engineer, in their own code, with no review.
The failure mode of the whole system. Cardinality is a self service footgun, so limits and rejection have to be built in rather than left to policy.
How long do we keep it, and at what resolution?
Days at full resolution, a year rolled up.
Retention as a pipeline of downsampling stages rather than a delete job. It also decides what a long range query is allowed to read.
Does alerting run on this same system?
Yes, and it must work while everything else is on fire.
A separate evaluation path with its own capacity. During an incident a hundred engineers are loading dashboards, and that is exactly when alerts must not queue behind them.
Is it allowed to depend on the platform it monitors?
No.
Separate cluster, separate identity, separate service discovery, and paging that leaves your infrastructure. A monitoring system that goes blind with the thing it watches is decoration.
Now drag the third slider. It represents one engineer adding one label to one metric, and it multiplies everything above.
Drag the third slider from 1 to 200 and watch. That slider is one engineer adding
customer_id to a metric so they can debug one account. It is a one line change, it passes
review, and it multiplies the entire system by two hundred.
Why cardinality hurts more than volume
Doubling the points per series costs you disk roughly linearly, and compression absorbs a
lot of it. Doubling the number of series doubles the index, doubles the open write buffers,
doubles the compression state held in memory, and makes every query touch twice as many
streams. Volume is a storage problem. Cardinality is a memory problem, and memory is the
thing you run out of suddenly rather than gradually.
scrape 10s1scrape 10s23Collectorhas the target listService A/metricsService B/metricsStorage
scrape 10s1scrape 10s23Collectorhas the target listService A/metricsService B/metricsStorage
The collector knows what should exist, so a target that stops responding is itself a signal: you can alert on a service being absent, which push can never do because a dead service simply stops sending. It also gives you natural back pressure, since the collector controls the rate. The cost is service discovery, and difficulty reaching things behind NAT or firewalls, or short lived jobs that finish before a scrape.
Why: The response reports how much work it did. In a system where one careless query can scan a hundred million points, making cost visible in every response is how expensive dashboards get found before they become an outage.
returns 204, or 429 with the offending metric named
Why: A rejection has to say which metric hit its series limit. A generic 429 during a cardinality explosion sends an on call engineer hunting through thousands of metrics at the worst possible time.
Why: Growth rate matters more than the absolute number. A metric at two million series and stable is fine. One at fifty thousand and doubling every ten minutes is the deploy that is about to take the system down.
scrape every 10s1targetsnew series?2accepted3downsample45older ranges6rules, every 30s789Servicesexpose /metricsService discoverywho exists right nowScraperspull, shardedCardinality guardlimit and rejectTime series DBsharded by seriesDownsampler1m, then 1hQuery layerpicks resolutionDashboardsand ad hoc queriesAlert evaluatorown capacityNotifieroff this clusterOn calla phone, ringing
Figure 1. Two paths leave the time series database and they are deliberately not the same path. Dashboards can queue behind each other during an incident. Alert evaluation cannot.
Walking Figure 1:
Scrapers pull from targets on a fixed interval. Service discovery tells them what
exists, which is the difference between monitoring a fleet and monitoring a list
somebody maintained by hand.
Every sample passes a cardinality check on the way in. A new label combination is a
new series, and a new series is a permanent cost, so this is where you refuse rather
than in a postmortem.
Accepted samples are appended to the shard that owns that series.
A downsampler produces coarser copies in the background. Nothing is deleted by a
query, only by retention.
The query layer chooses which resolution answers a given range.
Dashboards are served from there and are allowed to be slow.
Alert evaluation reads its own path with its own capacity. This is the arrow people
forget, and the one that matters at 3am.
Firing alerts leave through a notifier that does not live on this cluster.
Which is how the page gets out even when the platform you are monitoring is the thing
that is down.
A time series is one of the most compressible things in computing, and exploiting that is
what makes the whole system affordable.
Timestamps arrive at nearly fixed intervals, so store the delta of the delta, which is
usually zero and costs a bit or two. Values in a series usually change slowly, so XOR each
value with the previous one and store only the differing bits. Together these routinely get
a 16 byte point down to under two bytes.
series and chunkscolumn oriented, immutable chunks per time window
series_id
bigint
PK
A hash of the metric name plus its full sorted label set. This identity is the cardinality.
labels
inverted index
IDX
Label to series id postings, so a query filters without scanning. This lives in memory and is what cardinality inflates.
chunk
compressed block
Two hours of one series, delta of delta timestamps and XOR values. Immutable once closed.
head block
in memory
The current open window. Every active series holds one, which is the other memory cost of cardinality.
wal
append only
Write ahead log so a crash does not lose the head block.
Sample row
series 8814 = http_requests{svc="api",code="500"} | chunk 14:00-16:00 | 1,240 points in 2.1KB
Note where memory actually goes: the label index and one open head block per active series. Both scale with the number of series and neither scales with how long you retain data, which is exactly why cardinality and volume fail in completely different ways.
Your answer
An engineer adds a user id label to an existing metric so they can debug one customer. Which box in Figure 1 is supposed to stop them, and what happens to storage if it does not?
Series spread across metrics roughly evenly, because every metric is labelled by things with small bounded value sets: service, instance, status code, region. This is what you designed for.
The lesson is that you cannot prevent this at design time, so design for containment
instead. Per metric and per tenant series limits. A cardinality report that names the worst
offenders by team. And an alert on the growth rate rather than the absolute number,
because the absolute number looks fine right up until it does not.
Nobody queries per second data from eight months ago, and storing it costs the same as
storing today's.
Downsample on a schedule. Raw resolution for a couple of days, one minute for a month, one
hour for a year, and for each of those keep several aggregates rather than one: minimum,
maximum, sum, count. Keeping only the average is the mistake, because averaging averages of
different sized buckets is wrong, and the maximum is usually the number that mattered.
Figure 2. Data moves rightwards over time, getting coarser and cheaper. Queries pick the resolution from the range asked for.
A query for the last thirty minutes reads raw. A query for the last quarter reads hourly
rollups. The query layer picks based on the range and the pixel width of the chart, because
returning three million points to draw an eight hundred pixel wide graph is work nobody
benefits from.
During an incident, the metrics system is being hammered by every engineer loading
dashboards at once. That is exactly the moment alert evaluation must not slow down.
So separate them. Alert rules run on their own evaluators against their own read path, with
their own capacity, and they are not competing with a hundred people running expensive ad
hoc queries. A dashboard being slow during an incident is annoying. Alerts being late
during an incident means nobody knows there is an incident.
The deeper version of the same rule: the monitoring system must not depend on the systems
it monitors. If it authenticates through the same identity service, uses the same service
discovery, and runs on the same cluster, then a failure of any of those makes the system
blind precisely when it is needed. This is the question that separates people who have been
on call from people who have not.
Healthy. 10 million series, 20 million points a second, alerts evaluating every 15 seconds well inside their window. Compression is doing most of the work and nobody is thinking about the monitoring system.
Read every step as text
normal · Healthy. 10 million series, 20 million points a second, alerts evaluating every 15 seconds well inside their window. Compression is doing most of the work and nobody is thinking about the monitoring system.
cardinality spike · Degraded. A deploy adds a request id label to one metric. Series count climbs from 10 million towards 300 million over the next few minutes. Every new series allocates an index entry and an open head block, so ingester memory climbs far faster than disk usage does.
ingesters OOM · Down. Ingesters run out of memory and restart, losing head blocks and replaying write ahead logs, which is slow and memory hungry, so they run out again. The monitoring system is now down, which means nobody can see that anything else is wrong either. This is the worst outage in this design because it hides itself.
incident load · Down. A separate scenario with the same ending. A real incident starts, a hundred engineers open dashboards, and expensive queries saturate the read path. Alert evaluation shares that path and starts running late, so alerts for the actual incident fire minutes after they should.
contained · Degraded. Per metric series limits reject new label combinations past a threshold and alert loudly naming the metric, so one bad deploy degrades one metric instead of the system. Alert evaluators run on separate capacity with their own read path, so dashboard load cannot delay them. The offending metric is broken and everything else, including the ability to see that it is broken, still works.
What is the hardest part of running this at scale?
Open. The answer they are listening for is a specific word.
You
Cardinality. Volume is a storage problem and compression handles most of it, but every distinct label combination is a separate series with its own index entry, its own open write buffer and its own compression state, and all of that is memory. One engineer adding a user id label to an existing metric can multiply the series count by a thousand in a single deploy, and memory is the thing you run out of suddenly rather than gradually.
Names it in the first word, then explains the mechanism rather than just the symptom.
Interviewer
Can you prevent it?
The interesting follow up, because the honest answer is no.
You
Not at write time, because there is no way to tell an unbounded label from a legitimate one when the first few values arrive. So I would contain it instead: a per metric series limit that rejects new label combinations past a threshold and alerts naming the metric, plus a cardinality report ranked by growth rate rather than absolute count. One bad metric then degrades itself instead of the system.
Saying prevention is impossible and moving to containment is the senior move. Growth rate over absolute count is the detail that shows operational experience.
Interviewer
Push or pull for collection?
A preference question with one strong argument on the pull side.
You
Pull for anything long lived, mainly because absence becomes observable: the collector knows what should be there, so a target that stops responding is a signal, whereas with push a dead service looks identical to one that was never configured. Pull also keeps back pressure on the collector rather than letting a service in a retry loop flood ingest. I would add a push gateway for short lived batch jobs, and keep it clearly an exception rather than a second default.
The absence argument is the one that decides it, and it is stated first.
Interviewer
It is 3am, there is a major incident, and everyone is loading dashboards. What breaks?
The question that separates people who have carried a pager.
You
Alert evaluation, if it shares a read path with dashboards. A hundred expensive ad hoc queries saturate the query layer and alert rules start evaluating late, so alerts for the incident fire minutes after they should. I would give alert evaluators separate capacity and their own read path. More generally the monitoring system must not depend on what it monitors: same cluster, same identity service, same service discovery, and it goes blind exactly when you need it.
Answers the immediate case and then generalises to the independence principle. The generalisation is what makes it a staff level answer.
Interviewer
How long would you keep raw resolution data?
Checking whether retention is thought of as a pipeline or a setting.
You
A couple of days, then downsample to one minute for a month and one hour for a year. The important detail is keeping minimum, maximum, sum and count in each rollup rather than just the average, because averaging averages across unequal buckets is wrong and the maximum is usually the number someone actually cared about. I would set the exact windows from what people query, which I would measure rather than guess, since query ranges cluster far more tightly than anyone expects.
The min max sum count detail is small, specific and very hard to fake.
1. Why does adding a high cardinality label hurt more than simply storing more data points?
2. What can pull based collection detect that push cannot?
3. Why keep min, max, sum and count in a downsampled rollup rather than just the average?
Worth memorising
100,000 hosts with a few hundred series each is tens of millions of active series.
One careless label multiplies the series count, and series count is what kills these systems, not volume.
Every 10 seconds, forever. There is no quiet hour to run compaction in.
Alerting reads its own path. During an incident dashboards may be slow and alerts may not.
Say this in 60 seconds
The workload is relentless writes and bursty reads, and the thing that actually kills these systems is cardinality rather than volume, because every distinct label combination is a separate series holding an index entry, an open write buffer and compression state, all in memory. I would collect by pull for long lived services, since that makes absence detectable and keeps back pressure with the collector, with a push gateway as a marked exception for short jobs. Storage is immutable compressed chunks per time window using delta of delta timestamps and XOR values, which is roughly ten times smaller and is what makes this affordable at all. Retention is a downsampling pipeline, raw for days then minute then hour, keeping min, max, sum and count rather than just the average. I cannot prevent a cardinality explosion at write time, so I would contain it with per metric series limits that alert naming the offending metric, and I would run alert evaluation on separate capacity, because the moment everyone is loading dashboards is exactly the moment alerts must not be late.