Cardinality is the number of unique values a dimension can take. In observability it usually refers to the number of distinct time series a metric produces, which equals the number of unique combinations of its label values. High-cardinality attributes such as user IDs make data more detailed, but can sharply increase storage, memory and query costs.
What cardinality means
In mathematics, cardinality is the size of a set. In observability, the term is used in two closely related ways. First, the cardinality of an attribute is how many distinct values it has: http.request.method has a handful, user.id may have millions. Second, the cardinality of a metric is the number of distinct time series it produces. Every unique combination of label values creates its own series that must be stored, indexed and queried.
Consider a metric http_requests_total with the labels service (20 values), endpoint (50 values), status_code (8 values) and pod (30 values). In the worst case that is 20 × 50 × 8 × 30 = 240,000 time series for one metric name. In practice not every combination occurs, but the multiplication shows why one additional label can change the picture dramatically.
Why cardinality matters
Time-series databases such as Prometheus keep an index entry for every active series and hold recent samples in memory. Memory consumption, ingestion load and query latency therefore grow with the number of active series, not with the number of metric names. A "cardinality explosion" happens when a label suddenly takes unbounded values, for example when a request path containing an order ID is used as a label. Each new order creates new series, and memory use can grow until the database becomes slow or unstable.
Cardinality also drives cost. Many commercial metrics products price by the number of custom time series, so a careless label can multiply the bill. At the same time, high-cardinality data is precisely what makes debugging possible: knowing that errors affect one customer, one build or one Kubernetes node is often the key to the root cause.
Metrics versus events and traces
The tension between detail and cost depends on the data model. Metrics pre-aggregate data into time series, which is cheap for low-cardinality dimensions and expensive for high-cardinality ones. Logs and traces store individual events with arbitrary attributes, so a user.id on a span does not create a new time series. A common pattern is to keep metrics low-cardinality for dashboards and alerting, and put high-cardinality context on spans and structured logs, linked to metrics through exemplars.
Example: finding the culprit
In Prometheus you can list the metric names with the most series using a query such as:
topk(10, count by (__name__) ({__name__=~".+"}))
Suppose this shows that an illustrative metric api_latency_seconds_bucket has 1.8 million series. Breaking it down with count by (path) (api_latency_seconds_bucket) reveals that path contains raw URLs such as /orders/84721. Replacing the raw path with the route template /orders/{id}, as OpenTelemetry semantic conventions recommend for http.route, reduces the label to a few dozen values. Note that histogram metrics multiply cardinality further, because every bucket boundary is its own series.
Best practices
- Use bounded label values. Route templates instead of URLs, status classes instead of free-text error messages.
- Keep unbounded identifiers off metrics: user IDs, request IDs, session IDs and email addresses belong on traces and logs.
- Drop or aggregate labels at ingest. A telemetry pipeline can remove labels such as
podwhen per-pod resolution is not needed. - Monitor your cardinality. Track active series per metric and alert on sudden growth, the same way you watch any other resource.
- Agree on conventions. Shared attribute names and allowed values across teams prevent duplicate series with slightly different labels.
Common mistakes
The most common mistake is avoiding high-cardinality data altogether and losing the ability to answer "who exactly is affected?". The opposite mistake is adding every attribute to every metric "just in case". Another trap is churn: labels such as container IDs in Kubernetes change on every deployment, creating new series continuously even if the number of active series at any moment looks moderate.
How Obseria helps
Obseria Lake is an index-free lakehouse with schema-on-read, so high-cardinality attributes on logs and spans do not require predefined indexes. Obseria Pipeline can filter, aggregate or drop labels at ingest before they become costly, and usage-based pricing per signal makes the effect of those decisions visible. See reducing observability costs pipeline-first and cost management for related approaches.