How the maths works
Every number on this page follows from four steps, each shown separately above so you can check it by hand rather than trust a black box.
1. Active series
A Prometheus time series is identified by a metric name plus one specific combination of label values. If a metric has labels with 5, 6 and 20 possible values respectively, the number of distinct combinations is 5 × 6 × 20 = 600 — a product, not a sum, because every value of one label can pair with every value of every other label. A metric with no labels at all still produces exactly one series per target, because the metric name itself is an identity. Multiply that per-target series count by however many targets (pods, instances, replicas) independently expose the metric, since each target's scrape is stamped with its own identifying labels and so contributes its own separate series. Sum this across every metric you instrument and you have total active series.
2. Samples per second
Every active series is scraped once per scrape interval, so the ingestion rate the Prometheus head block has to absorb continuously is simply active series divided by the scrape interval in seconds. A site with 20,000 active series scraped every 15 seconds is ingesting roughly 1,333 samples every second, forever, for as long as that instrumentation stays live.
3. Total samples over retention
Samples per second is a rate; what actually sits on disk is the accumulation of that rate over the whole retention window. Multiply samples per second by retention in seconds and you have the total sample count the TSDB is holding at steady state — not a peak, a running total across every block that has not yet been deleted by the retention policy.
4. Disk
Disk is kept as two separate, added numbers rather than one blended constant, because they come from genuinely different sources and behave differently under churn:
- Sample bytes = total samples × bytes-per-sample. This is the number
Prometheus's own storage documentation addresses directly: “Prometheus stores an average
of only 1-2 bytes per sample”, and gives the exact capacity-planning formula this tool's
first two steps mirror:
needed_disk_space = retention_time_seconds × ingested_samples_per_second × bytes_per_sample. That 1–2 byte figure describes well-behaved, slowly-changing values under Prometheus's delta-of-delta chunk compression. It is a real, cited range — and also genuinely variable, which is why this tool makes it an input with a default rather than a hidden constant. - Index bytes = active series × churn multiplier × index-bytes-per-series. Every series, however few samples it contributes, needs an entry in the index that maps label combinations to the chunks holding their data. That cost scales with how many distinct series have existed over the retention window, not with how many samples they produced — which is exactly why heavy churn (short-lived series from restarting pods, rolling deploys, ephemeral job IDs) inflates real-world disk usage well beyond what the samples-per-second maths alone would predict, even though it barely moves the active-series count at any single instant.
Why churn gets its own factor instead of being folded into bytes-per-sample
Bytes-per-sample and churn are different failure modes and conflating them hides which one is actually the problem. A metric with stable, long-lived series and noisy values inflates bytes-per-sample but leaves the index alone. A metric with clean, compressible values but a label like a pod name that changes on every restart inflates the index while sample bytes stay near the cited 1–2 byte range. Keeping them as two visible terms means the receipt above tells you which one to go fix, instead of one number that could mean either.
Why high cardinality hurts — not just “a lot of numbers”
Prometheus's own instrumentation guidance does not treat this as a vague aesthetic preference. It says plainly: “Each labelset is an additional time series that has RAM, CPU, disk, and network costs.” Two concrete mechanisms sit behind that sentence:
- Memory. Every active series keeps recent samples in the in-memory head block before they are written to disk. More series means more of that memory held simultaneously, regardless of how few samples each individual series contributes — a metric with a million rarely-updated series can cost more resident memory than one with a thousand busy ones.
- Query latency. A PromQL query with a label selector still has to find every series matching that selector before it can aggregate, filter or rate() them. A query against a metric with 100,000 series does meaningfully more work than the same query against one with 100, even if the final answer — a single summed number — looks identical either way.
The same guidance gives a concrete illustration of where this goes wrong: instrumenting a per-user quota check across a fleet of 10,000 nodes, with 10,000 possible users, reaches “a double digit number of millions” of series — which it states directly “is too much for the current implementation of Prometheus.” That is not a hypothetical; it is Prometheus's own documentation describing the exact shape of mistake the worked example below reconstructs at a smaller scale.
The same source gives a concrete per-label guideline worth repeating verbatim, because it is more specific than most engineering rules of thumb get: “As a general guideline, try to keep the cardinality of your metrics below 10, and for metrics that exceed that, aim to limit them to a handful across your whole system. Only a small number of metrics should exceed 100.” This tool's own danger indicator (10,000 and 100,000 total series for one metric) is not from that document — it is this tool's own reasoning, applying the per-label guideline multiplicatively across a realistic label set, stated as such rather than attributed to Prometheus.
Three worked examples
1. A small, healthy service — and one histogram that quietly isn't
Twenty replicas of a typical HTTP service, each exposing five metrics: http_requests_total
(method: 4 values, status: 5 values, route: 15 values), a duration histogram on the same
dimensions with an 11-bucket le label, and three near-constant process metrics with no
labels at all.
| Metric | Label product | × targets | Series | Tier |
|---|---|---|---|---|
| http_requests_total | 4×5×15 = 300 | 20 | 6,000 | Normal |
| http_request_duration_seconds_bucket | 4×15×11 = 660 | 20 | 13,200 | Watch this |
| process_cpu_seconds_total | 1 | 20 | 20 | Normal |
| go_goroutines | 1 | 20 | 20 | Normal |
| up | 1 | 20 | 20 | Normal |
Total: 19,260 active series, 1,284 samples/sec at a 15-second scrape interval, roughly 1.66 billion samples over a 15-day retention window, and about 2.17 GB of estimated disk at the default 1.3 bytes/sample and 100 index-bytes/series (2.16 GB of that is samples, 1.93 MB is index). Nothing here is dangerous, but notice that the duration histogram alone — not the counter it measures the same thing alongside — is already the single largest contributor to the whole service's series count, purely from its 11 buckets. That is the pattern worth watching, not the total.
2. A 200-node fleet, where one metric crosses into the danger tier on its own
The same shape scaled to a 200-node fleet running node_exporter plus the same HTTP service:
node_cpu_seconds_total (cpu: 16 values, mode: 10 values), node_filesystem_avail_bytes
(device: 5 values, mountpoint: 5 values), and http_requests_total (method: 5, status: 6,
route: 20) — each exposed by all 200 targets, at a 30-day retention and a modest 1.5×
churn multiplier for rolling deploys.
| Metric | Label product | × targets | Series | Tier |
|---|---|---|---|---|
| node_cpu_seconds_total | 16×10 = 160 | 200 | 32,000 | Watch this |
| node_filesystem_avail_bytes | 5×5 = 25 | 200 | 5,000 | Normal |
| http_requests_total | 5×6×20 = 600 | 200 | 120,000 | High cardinality |
Total: 157,000 active series, 10,467 samples/sec, about
27.1 billion samples over 30 days, and roughly 35.3 GB
of estimated disk. The interesting line is http_requests_total crossing 100,000 series
on its own, purely from three ordinary-looking labels multiplied across 200 targets — no
single label here looks unreasonable, but 5 × 6 × 20 × 200
is where ordinary numbers compound into a large one.
3. “This is what killed our Prometheus” — adding one label
Start with the same http_requests_total across 10 targets: method (5), status (6),
route (20). That is 5×6×20 = 600 combinations, ×10 targets =
6,000 series — comfortably in the normal range. Then someone, trying to debug
a slow customer, adds a fourth label: user_id, with roughly 50,000 distinct values.
| State | Label product | Series | Samples/sec | Disk (15d) |
|---|---|---|---|---|
Before user_id | 600 | 6,000 | 400/s | 675 MB |
After user_id (50,000 values) | 30,000,000 | 300,000,000 | 20,000,000/s | 33.7 TB |
One label turned 6,000 series into 300 million, and 675 MB of estimated 15-day disk into 33.7 TB. Twenty million samples a second is not a number any single Prometheus instance ingests — long before disk became the visible problem, the head block would have exhausted available memory and the process would have been killed by the operating system's out-of-memory handler, which is usually how this actually gets discovered in production: not as a graph of disk climbing, but as a crash. Try it yourself with the “Load worked example” and “Add a user_id label” buttons above — the series count updates the instant the label is added.
The fix is never to make the estimator's numbers smaller by adjusting bytes-per-sample. It is to remove the unbounded label: log the user ID alongside the request instead of attaching it to a metric, or replace it with something bounded — a customer tier, an account cohort — if the dimension genuinely needs to be queryable in Prometheus at all.
Frequently asked questions
What counts as “high cardinality” in Prometheus?
There is no single official cutoff, and this page does not pretend there is one. Prometheus's own instrumentation guidance says to keep the cardinality of an individual label below 10 as a general guideline, and to treat anything over 100 distinct values for one label as a signal to investigate alternatives. This tool applies that per-label guidance multiplicatively and flags a metric's total series count at two wider bands — 10,000 and 100,000 — as points where the resulting series count is commonly discussed as starting to cost real memory and query time. Treat both numbers as orientation, not as a certified limit your specific deployment will hit at exactly that line.
Why does adding one label multiply, not add, my series count?
Because a Prometheus time series is identified by the metric name plus its entire set of label values together, not by any one label alone. Every distinct combination is a separate series. Two labels with 10 values each do not give you 20 possible series; they give you 10 × 10 = 100, because each of the first label's values can pair with each of the second's. Add a third label with 1,000 values and it becomes 100 × 1,000 = 100,000. This is exactly why one unbounded label — a user ID, a request ID, a raw email address — can turn a modest metric into a very large one on its own; see the worked example above.
Is 1–2 bytes really enough to store one sample?
Prometheus's own storage documentation states it plainly: “Prometheus stores an average of only 1-2 bytes per sample.” That figure is real, but it describes compressed chunks under typical conditions — slowly changing values, Gorilla-style delta-of-delta encoding doing its job. It is not a law of physics. Highly volatile values, frequent series churn, and label sets that force many small chunks all push the true figure higher. That is exactly why this tool exposes bytes-per-sample as a field you can change rather than a constant baked into the arithmetic — the default of 1.3 is a reasonable midpoint of the cited range, not a promise about your deployment.
What is “churn” and why does it matter separately from active series?
Active series counts what exists right now. Churn counts what has existed over the whole retention window — every series that appeared and then stopped, most commonly because a pod, container or ephemeral instance restarted with a new identifier in one of its labels. Prometheus keeps a small index entry for a churned series until the block containing it is eventually compacted away, so heavy churn inflates the index well beyond what the active-series count alone suggests, even though it barely changes samples per second. The churn multiplier here is a deliberately simple stand-in for that effect: a value of 3 means “assume the index has to account for roughly three times as many distinct series as are active at any one instant.” The default of 1 assumes no meaningful churn, which is optimistic for anything running in Kubernetes with autoscaling or frequent deploys.
Why do histogram buckets multiply cardinality so fast?
A Prometheus histogram is implemented as one series per bucket boundary, each carrying an
le label naming that boundary, plus _sum and _count series.
Twelve buckets is not one metric with twelve values — it is twelve additional series for every
other combination of labels that metric already has. In the first worked example above, adding an
11-value le label to a metric that already had 60 combinations from its other labels
turns 60 series into 660, before any target multiplier is even applied. Histograms are usually still
the right instrumentation choice — they answer questions counters and gauges cannot —
but the bucket count is a cardinality decision like any other label, and this tool treats
le exactly like any other label so the arithmetic is visible.
Does this send anything to my Prometheus server, or to this site?
No. There is nothing here that could — the tool never asks for a Prometheus URL, a bearer token or any credential, and it has no code path capable of making a network request. The metric names, label names and cardinality estimates you type exist only as JavaScript variables in this tab. Open your browser's developer tools, switch to the Network panel, and add a metric: no request appears, because the arithmetic is plain multiplication and addition running locally, the same as a spreadsheet open on your own machine.
Is the index-bytes-per-series number as solid as bytes-per-sample?
No, and this page is explicit about that difference. Bytes-per-sample has a specific, citable range from Prometheus's own documentation. Index overhead per series does not have an equivalent published constant — it depends on label name and value lengths, how many distinct label combinations share index structures, and how compaction has run. The 100-byte default here is an order-of-magnitude placeholder for reasoning about scale, adjustable like every other input, not a number lifted from a specification. For anything beyond a rough sizing conversation, measure a real Prometheus instance's block sizes on disk directly.
What should I actually do if a metric is flagged high cardinality?
First, find which label is responsible — it is very often exactly one of them, not all of them together. Ask whether that label's values form a bounded, known-in-advance set (HTTP methods, a handful of status-code buckets, a fixed list of regions) or an effectively unbounded one (user IDs, raw paths with embedded IDs, email addresses, request IDs). Bounded labels are usually fine even with a moderate value count. Unbounded ones should either be dropped from the metric entirely, replaced with a coarser bucketed version, or moved to a system built for high-cardinality exploration — logs or traces — rather than forced into a metrics label. Prometheus's own naming and labels guidance says exactly this: “Do not use labels to store dimensions with high cardinality (many different label values), such as user IDs, email addresses, or other unbounded sets of values.”
Does replica count really multiply series, or do replicas share series?
It multiplies, because Prometheus scrapes each target independently and every target's scrape is
stamped with that target's own instance and job labels (or whatever
relabeling assigns), which makes each replica's exposition a distinct series even when every other
label matches exactly. Ten identical replicas of a service each exposing http_requests_total
with the same method and status values do not collapse into one series — they become ten. That
is why “targets” is a direct multiplier in this tool's model rather than something that
only affects sampling rate.
Can I use this for real capacity planning, or only for rough intuition?
Treat it as the second: a way to reason about orders of magnitude and catch an obviously
dangerous design — like an unbounded label — before you ship it, not as a substitute for
measuring an actual Prometheus instance. The active-series and samples-per-second numbers follow
directly from arithmetic you can verify by hand. The disk estimate is softer by construction,
because bytes-per-sample and index overhead both vary with real-world data in ways a calculator
cannot see in advance. Once a service is live, Prometheus's own prometheus_tsdb_head_series
and prometheus_tsdb_storage_blocks_bytes metrics give you the true numbers to compare
this estimate against.
Use cases and limitations
What people use this for
- Reviewing instrumentation before it ships. Paste the label set a developer is proposing and see the series count before it reaches production, where the only feedback loop otherwise is an incident.
- Sizing a new Prometheus deployment. Rough out disk and memory needs from an inventory of planned metrics, targets and retention before provisioning hardware or a persistent volume.
- Explaining a cardinality incident after the fact. Reconstruct which label was responsible and by how much, in numbers a non-specialist teammate can follow.
- Comparing retention or scrape-interval trade-offs. See directly how halving the scrape interval or doubling retention moves total samples and disk, without needing a live Prometheus instance to experiment against.
- Teaching the multiplication rule. The worked “before/after user_id” example is deliberately built to be shown to a teammate who has not internalised why one label can matter this much.
What it deliberately does not do
- Connect to a real Prometheus instance. There is no scrape endpoint field, no API token field, and no code path that could use one. Every number here is a manual estimate you supply, not a live measurement.
- Model query-time cost precisely. The danger indicator explains why high cardinality slows queries, but does not attempt to estimate a specific query's latency, which depends on the query itself, not just the series count of one metric.
- Account for recording rules, remote write, or federation. Those each change the effective series and sample counts a given Prometheus instance handles, and are outside this tool's model of a single scrape-and-store pipeline.
- Pretend the disk number is exact. Active series and samples/sec follow from arithmetic you can check by hand. Disk depends on bytes-per-sample and index overhead, both of which are explicitly modelled as adjustable, cited-where-possible estimates rather than facts.
Honest limitations
This tool has no way to know your actual label value distributions, how compressible your specific values are, or how your compaction schedule behaves — it only knows the numbers you type in. Two services with identical series counts can have meaningfully different real disk usage if one has volatile floating-point values and the other has mostly-static gauges. Treat every disk figure here as an order-of-magnitude estimate to sanity-check a design, and treat the active-series and samples-per-second figures — which follow from exact arithmetic rather than a cited approximation — as the numbers you can rely on most.
- What is high cardinality in Prometheus?A deeper look at why label combinations explode and how to spot the label responsible
- How much disk does Prometheus actually need?Working through the storage formula with real retention and scrape-interval trade-offs
- About this toolWhy it exists, and what it deliberately does not do