5 — Label & Attribute Schema Design
Cardinality explains why an unbounded label is expensive. This chapter is the design discipline for not shipping one in the first place — what to name a label, which values are safe to put on a metric at all, and the handful of specific traps that account for most real cardinality incidents.
What a label actually costs
Every unique combination of label values on a metric creates a new time series. A metric with three labels of cardinality 10, 5, and 4 doesn’t cost 10+5+4 — it costs up to 10×5×4 = 200 series, because the cost is the combination, not any single label in isolation. See What is Cardinality (in observability) for the full mechanics of why this multiplies rather than adds, and how it shows up as storage and query cost downstream.
This chapter assumes that mechanic and focuses on the decisions that keep the multiplication bounded in the first place.
Naming: let semantic conventions decide first
OpenTelemetry’s semantic conventions already define
the namespaced attribute name for anything common — http.request.method, db.system.name,
k8s.pod.name. If a semconv attribute already exists for what you’re about to name, use it; don’t
invent httpMethod or db_type next to it. The design decision this chapter is actually about is
everything semconv doesn’t cover: your own business and domain attributes — tenant.tier,
order.fulfillment_type, checkout.payment_provider.
For those, the same discipline semconv applies to itself works for a custom namespace too:
- Namespace by domain, dot-separated —
checkout.payment_provider, notpayment_providerbare, so two unrelated teams’ attributes don’t collide and a reader can tell where an attribute came from without checking the emitting service. - Name the dimension, not the instance —
regionis a label; a raw region string that happens to be one of ten fixed values is fine, but if “region” is actually free text a user typed, it’s a different, unbounded dimension wearing the same name. - Prefer enums over free text — a label whose legal values are a small, known, stable set (an enum, a status code, a boolean) is what makes a label safe. The moment a label’s value comes from anything a user, a request, or an external system can generate arbitrarily, it stops being a label candidate.
The cardinality budget
A cardinality budget is a ceiling — active series per metric, per service, or per tenant — treated as a resource to spend deliberately, the same way a latency or error-rate budget is spent deliberately in an SLO. Two labels that are individually “only 20 values each” can still blow a budget once combined with three other labels already on the same metric; the budget has to account for the full combination a metric actually ships with, not each label reviewed in isolation.
Getting the actual number — active series, ingest rate, and monthly cost for a specific label set — is a mechanical calculation, not a design judgment call. Run it through the Cardinality Budget Calculator skill before any new label ships into a production-bound config; this chapter is about the judgment calls that decide what to feed that calculator in the first place.
The high-churn label traps
These five show up in almost every real cardinality incident, because each one looks like a reasonable label until you notice its value is different on every single request:
| Trap | Why it looks reasonable | Why it explodes | Where it belongs instead |
|---|---|---|---|
| Request ID / correlation ID | ”I want to find this exact request later” | Unique per request, by construction — infinite cardinality | A log field, or a metric exemplar’s trace_id |
| User ID / session ID | ”I want to see this user’s error rate” | Cardinality scales with active user count, unbounded | A log field; aggregate the metric by cohort/tier instead |
| Raw timestamp | ”I want to know exactly when” | The metric’s own time series index already carries this | Nowhere — it’s redundant with the sample’s own timestamp |
| Client/source IP address | ”I want to see traffic by origin” | One series per distinct IP, unbounded at any real scale | A log field, or bucket to a coarse dimension (ASN, region) |
| Full URL / query string | ”I want per-endpoint latency” | Query params and path variables make every URL near-unique | A normalized route template (/users/{id}), not the raw path |
The fix in every row is the same shape: the information isn’t wrong to want, it’s on the wrong
signal. The Signals covers why — a log or a trace span can carry a value that’s
unique per request at no extra cardinality cost, because neither is indexed by label combination the
way a metric is. An exemplar is the specific mechanism that lets a metric point at one such trace
without ever putting trace_id on the metric itself — see 3 — Cross-Signal Correlation.
Where this gets enforced, not just designed
Naming and cardinality discipline decays without something checking it. Two different mechanisms, usually both needed:
- A canonical schema teams are onboarded against — see Naming & Label Schema for what this looks like as an actual onboarding contract, not just a convention doc nobody reads.
- A governance backstop that catches drift after onboarding — see Cardinality Governance for the FinOps side: budgets, alerting on unexpected series growth, and what happens when a team exceeds its allocation.
Why this matters for an Observability Architect
A cardinality incident is almost never one obviously-bad label — it’s several individually defensible labels combining on a metric nobody reviewed as a whole. Reviewing a new metric means asking about the combination (“what’s the cross-product of every label on this one metric”), not just auditing each label name against the trap list above. The trap list catches the obvious cases; the combination question catches the ones that pass every individual review and still explode.
Metadata
| Dimension | Detail |
|---|---|
| Author | Amit Singh |
| Scope | observability |
Local graph
Linked from 16 notes
8 — Log Aggregation
Schema-on-write vs. schema-on-read as competing bets about when to pay indexing cost, and the two different deduplication problems a log pipeline actually has to solve.
1 — What Observability Actually Means
Observability vs. monitoring, the three-pillars critique, and why observability is a property of how a system was instrumented — not a tool you bought or a dashboard you built.
2 — The Signals
Metrics, logs, traces, profiles, and events — what each is built to capture, what it costs, and which question it actually answers vs. which one people mistakenly ask it.
1 — OpenTelemetry SDKs & Semantic Conventions
OpenTelemetry is a specification and an API/SDK, not a backend — the pieces that make it up, and the semantic-convention vocabulary that lets two unrelated teams' telemetry be queried the same way.
4 — Auto vs. Manual Instrumentation
Four ways a span gets created — hand-written, framework-level auto-instrumentation, eBPF, and service-mesh sidecar capture — and the trade-off between code changes and business context each one makes.
9 — OTel Collector Pipeline Design
Receivers, processors, and exporters chained into a pipeline; why a platform runs more than one; and the agent/gateway topology that tail sampling specifically forces on that design.
4 — Observability-Driven Development
The TDD analogy taken seriously: SLOs and instrumentation defined at design time as acceptance criteria, not retrofitted after an incident — and why this only sticks as a launch gate, not a guideline.
What is Cardinality (in observability)
The number of unique time series (or unique log/trace label combinations) a metric produces — the single biggest driver of ingest cost and query latency in Prometheus-family backends (Mimir, Cortex, Thanos), and the reason unbounded labels are a production incident waiting to happen.
2 — Labels and Cardinality
Label mechanics and series identity in Prometheus — how labels turn one metric name into many time series, the storage/performance cardinality math, and target relabeling vs. metric relabeling with real relabel_configs YAML.
7 — Multi-Tenancy
Two separate guarantees hiding under one name — data isolation and performance fairness — and the tenant identification, quota enforcement, and selective backpressure that make both hold under shared infrastructure.
7 — Metrics Storage (TSDB)
Chunk/block encoding, the write-ahead log, compaction and the write amplification it trades for query speed, and why a cardinality spike is a storage-engine problem, not just a cost line item.
1 — Building a Platform Team
A platform team's product is other teams' ability to self-serve reliable telemetry — team topology, the paved road that makes everything earlier in this book the default instead of a manual step, and the ticket-queue failure mode to watch for.
Related notes
4 — Auto vs. Manual Instrumentation
Four ways a span gets created — hand-written, framework-level auto-instrumentation, eBPF, and service-mesh sidecar capture — and the trade-off between code changes and business context each one makes.
1 — OpenTelemetry SDKs & Semantic Conventions
OpenTelemetry is a specification and an API/SDK, not a backend — the pieces that make it up, and the semantic-convention vocabulary that lets two unrelated teams' telemetry be queried the same way.
9 — OTel Collector Pipeline Design
Receivers, processors, and exporters chained into a pipeline; why a platform runs more than one; and the agent/gateway topology that tail sampling specifically forces on that design.
10 — Collector Pipelines
Covers composing multiple named pipelines in one Collector for signal-specific or team-specific routing.