Dropping and Aggregating Metrics in the Collector

A metrics bill is series count multiplied by sample interval, and series count is decided by labels. That makes collector-side trimming unusually effective: removing one label divides the series count by that label's cardinality, which is a factor of fifty rather than a percentage. This page covers finding the series that cost the most, removing them without losing the metric, and verifying that nothing anybody queries breaks. It is a task article under telemetry cost and data volume control, part of the Python telemetry pipelines and delivery section, and it pairs with controlling label cardinality in Prometheus.

Removing a label divides, it does not subtract One latency histogram is shown with four labels: route with twenty-four values, method with four, status code with six, and pod name with forty. Multiplied together and by twelve histogram buckets, that produces two hundred and seventy-six thousand four hundred and eighty series from a single instrument. Removing the pod label by summing over it leaves the same metric with the same totals and six thousand nine hundred and twelve series, a reduction of ninety-seven and a half percent achieved by deleting one word from a configuration file. The comparison notes that no equivalent reduction is available from any sampling or interval change, because those act linearly while a label acts multiplicatively, and that the pod label is almost always the one to remove first because a pod name is both high cardinality and short-lived. one histogram, four labels route · 24 × method · 4 × status · 6 × pod · 40 × buckets · 12 = 276 480 series from one instrument sum over pod, keep everything else route · 24 × method · 4 × status · 6 × buckets · 12 = 6 912 series · same totals · 97.5% removed by deleting one word sampling acts linearly · a label acts multiplicatively — which is why this is the first place to look
No sampling rate can match what removing one high-cardinality label does, because one is a percentage and the other is a divisor.

Prerequisites

pip install "prometheus-client>=0.20.0,<1.0.0" \
            "opentelemetry-sdk>=1.27.0,<2.0.0"

Implementation

Step 1 — Rank metrics by series count and check which are queried. Two lists, intersected. The first is the metrics store's own count of series per metric name, which every store exposes. The second is the set of metric names appearing in dashboards and alert rules, which can be extracted from their definitions. A metric high on the first list and absent from the second is pure cost, and in most fleets there are several.

# the series count per metric, most expensive first
topk(20, count by (__name__) ({__name__=~".+"}))

Step 2 — Drop whole metrics nothing references. This is the cheapest possible reduction: no capability is lost because no capability was being used. Instrumentation libraries are the usual source, since they emit a broad default set and most fleets use a handful of them.

processors:
  filter/drop_unused:
    error_mode: ignore
    metrics:
      metric:
        - 'name == "http_server_active_requests"'
        - 'name == "process_runtime_cpython_cpu_time"'
        - 'IsMatch(name, "^rpc_client_.*")'

Step 3 — Aggregate away a label rather than dropping the metric. Where the metric is used but one of its labels is not, summing over that label keeps every total exactly correct while dividing the series count. This is almost always better than dropping the metric, and it is the single highest-leverage action available.

processors:
  metricstransform/trim_pod:
    transforms:
      - include: http_server_duration
        action: update
        operations:
          - action: aggregate_labels
            label_set: [http_route, http_request_method, http_response_status_code]
            aggregation_type: sum

Step 4 — Rewrite unbounded values into bounded ones. Sometimes the label is genuinely wanted and its values are the problem: a raw URL path, a customer identifier, an error message. Rewriting the value — a path to its route template, an identifier to a tier, a message to an error class — keeps the dimension with a value set small enough to be affordable.

processors:
  transform/bound_labels:
    metric_statements:
      - context: datapoint
        statements:
          # /orders/8812 -> /orders/{id}
          - replace_pattern(attributes["http.route"], "/[0-9]+", "/{id}")
          # keep a tier, not a customer
          - set(attributes["tenant_tier"], "enterprise") where attributes["tenant"] != nil
          - delete_key(attributes, "tenant")

Step 5 — Add a safety limit for runaway series. Trimming known problems does not prevent new ones. A limit that starts discarding new series past a threshold turns a cardinality explosion from an outage of the metrics store into a bounded loss of one metric, which is a much better failure. It should alert loudly when it engages, because it is a symptom rather than a solution.

Step 6 — Run both pipelines until the queries are verified. A trimmed metric that breaks an alert is discovered at the worst moment. Exporting both the trimmed and untrimmed versions to different destinations for a week, and checking every dashboard and alert against the trimmed one, costs a week of double storage and avoids the alternative.

Expected Output: the series count falling sharply with no change in any rate or quantile query.

before   active series 284_902   ingest 41.2 MB/min
after    active series   9_841   ingest  2.1 MB/min
query    sum(rate(http_server_duration_count[5m]))  unchanged: 418.2
Four ways to cut, and what each costs Four reduction techniques are compared on two axes expressed as annotations. Dropping a metric nothing queries removes its entire series count and costs no capability at all, making it the first thing to do. Aggregating away an unused label divides the series count by that label's cardinality and costs only the ability to break the metric down by that dimension, which by assumption nobody was doing. Rewriting an unbounded label value into a bounded one keeps the dimension in a coarser form, removing most of the series while retaining a usable breakdown. Lengthening the collection interval reduces samples rather than series, so it produces a linear saving on ingest and none on storage of the series themselves, and it costs resolution everywhere including where it matters. The ordering makes the point that the cheapest reductions are also the largest, and the one people reach for first is the one at the bottom. ranked by saving, which happens to be ranked by how little it costs 1 · drop a metric nothing queries removes 100% of it · costs nothing 2 · aggregate away an unused label divides by its cardinality · costs one breakdown 3 · bound a label's values keeps a coarser version of the dimension 4 · longer interval linear on samples, none on series · costs resolution everywhere
The reductions with the largest effect are also the ones that cost the least. The instinct to lengthen the scrape interval is the weakest lever on the list.

Why this belongs in the collector

There is a reasonable objection to all of this: the correct fix is to stop the application emitting the label in the first place, and everything here is a workaround. Both halves of that are true, and the workaround is still worth having for three reasons.

Speed. A cardinality explosion is an operational event with a live impact on the metrics store, and it needs a response in minutes. A collector configuration change is minutes; a release across every affected service is days. Having the mechanism ready before it is needed is the difference between a contained incident and an extended one.

Reversibility. Removing an instrument from application code is a decision that is expensive to revisit — the data for the intervening period simply does not exist. A collector-side aggregation can be removed and the full series resume immediately, because the application never stopped producing them. That makes it a much safer thing to try.

Fleet-wide consistency. Forty services that each emit a slightly different set of labels can be normalised in one place. Achieving the same thing in application code means forty changes, agreed and shipped in step, which in practice means it does not happen.

The honest caveat is that collector-side trimming hides the problem from the people creating it. A service whose cardinality is being cleaned up downstream has no feedback that it is doing anything wrong, and will do it again. Pairing the trimming with a report — these metrics are being aggregated for you, and here is what it is costing — keeps the fix visible, and is usually what eventually produces the proper repair in the instrumentation.

Finding the label before it finds you

The techniques above assume the problem has already been identified. Identifying it quickly is a skill worth having, because the first symptom is usually a metrics store under memory pressure rather than a helpfully named error.

The fastest route is to rank metric names by series count, take the top entry, and print the label set of a single one of its series. In the overwhelming majority of cases the offending label is visible at a glance: a pod name, a full URL, an identifier, a message string, a version tag that changes on every build. There is rarely any subtlety to it — the label that is wrong looks wrong.

The second check is whether the series count is growing or stable. A stable high count is an instrumentation decision that can be trimmed at leisure. A count that grows monotonically is an active problem, because the store never forgets a series until retention expires, and something is creating new ones continuously. Growth that tracks request rate means a per-request value became a label; growth that tracks deployments means a build identifier or a pod name did.

The third check is which service is responsible, which matters because the fix eventually belongs there. Grouping the series count by the service label gives it directly, and it is worth doing even when the collector-side trim is applied immediately, because the report back to that team is what stops the next occurrence. A cardinality problem fixed only in the collector will recur with the next metric that team adds.

Which collector processor for which cut A table of metric volume reductions and the collector processor that performs each. Dropping whole metrics nobody queries: the filter processor with a metric name match. Dropping a high-cardinality label and summing across it: the transform processor with aggregation, or the metricstransform processor's aggregate_labels. Removing a label without re-aggregating: the attributes or transform processor, which is only safe when the remaining labels still identify unique series. Reducing resolution by exporting less often: the interval processor or a longer export interval. The note says removing a label without aggregating creates duplicate series that collide in the backend. cut processor drop unused metrics filter, by metric name drop a label and sum transform / metricstransform aggregate delete a label only attributes — risky: series collide lower resolution interval processor, longer interval deleting a label without aggregating makes duplicate series collide
Dropping whole metrics is safe. Dropping a label is safe only when the series are re-aggregated at the same time.

Configuration options

Technique Processor Series effect Capability cost
Drop a metric filter removes all of it none, if unqueried
Aggregate a label away metricstransform divides by cardinality one breakdown
Bound a label's values transform divides by the reduction finer breakdown
Drop a data point conditionally filter proportional those conditions
Series limit pipeline limit caps the worst case the newest series
Longer interval reader config samples only resolution

Verification

Confirm the totals are unchanged, because aggregation preserves them and a mistake does not.

# the same query against the trimmed and untrimmed destinations
for host in metrics-full metrics-trimmed; do
  curl -sG "http://$host:9090/api/v1/query" \
    --data-urlencode 'query=sum(rate(http_server_duration_count[5m]))' \
    | python3 -c 'import json,sys; print(json.load(sys.stdin)["data"]["result"][0]["value"][1])'
done

Expected Output: identical values, to the precision the sampling interval allows.

418.21
418.21

A difference means the aggregation is summing something it should not, most often a gauge being summed where it should have been averaged — a mistake that is invisible in the series count and obvious in this comparison.

Common mistakes

Summing a gauge across a removed label. Error signature: a gauge whose value is now several times too large. Root cause: sum applied where the label distinguished instances of the same measurement. Remediation: use an appropriate aggregation, or keep the label for gauges where instance identity is the point.

Dropping a metric an alert depends on. Error signature: an alert that silently never fires again. Root cause: the queried-metric list was taken from dashboards only. Remediation: extract metric names from alert rules as well, and run both pipelines until verified.

Trimming without telling anyone. Error signature: an engineer spending an afternoon on a breakdown that no longer exists. Root cause: a collector change invisible from the application side. Remediation: document what is aggregated where, and report it back to the owning teams.

Relying on a series limit as the fix. Error signature: a metric that works for some label values and not others, unpredictably. Root cause: a limit discarding whichever series arrived last. Remediation: treat the limit as a circuit breaker with a loud alert, and fix the cardinality it caught.

Reaching for the scrape interval first. Error signature: a small saving and noticeably worse resolution. Root cause: interval affects samples, not series, and series dominate the cost. Remediation: work the list above in order.

Frequently Asked Questions

Why trim metrics in the collector rather than in the application?

Because it takes effect fleet-wide in minutes and can be reversed just as quickly. Changing instrumentation means a release per service, which is the right fix eventually and useless when a cardinality explosion is happening now.

Does aggregating away a label lose data?

It loses the ability to break that metric down by the removed dimension, and nothing else. The totals remain exactly correct because the values are summed rather than sampled, which is why aggregation is usually preferable to dropping the metric.

What is the fastest way to find a cardinality problem?

Rank metrics by series count and look at the top of the list. A single metric accounting for the majority of a fleet's series is the normal shape of this problem, and the label responsible is almost always visible immediately once you look at one series' labels.

Can the collector cap cardinality automatically?

It can drop data points whose attributes match a pattern, and some pipelines add a limit processor that starts discarding new series past a threshold. Neither is a substitute for fixing the instrumentation, but both stop a runaway from taking down the metrics store.