Click any words you think contain an error. Click again to unmark.
Two bugs: emails as span attributes (PII plus unbounded cardinality) and a 50 KB prompt blob in an indexed attribute (index bloat); fix with opaque userId on the trace and span events for prompt content.
Imagine a library card catalog. Every book gets one card with its title, author, and a few index tags so people can find it fast. Now imagine someone stuffs an entire 50-page chapter onto each card, and writes every reader's full email on the card too. The catalog drawers stop closing, queries crawl, and there is private data on every card. Span attributes are the card catalog; they are tiny and indexed. Big content goes in span events or a separate store, and personal data needs a stable opaque id.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Trace cardinality and trace-attribute hygiene are the two most expensive mistakes in early LLM-observability instrumentation. They look harmless in code review (one extra set_attribute call), they pass functional tests (the trace shows up in the UI just fine), and the bill arrives quietly weeks later as ingest slowdowns, storage cost creep, and a compliance flag.
This spot-error walks through both bugs in one snippet, why each is a real production problem, and what the corrected shape looks like in 2026 OTel-shaped instrumentation.
Mental model: span attributes are a filter index. Span events are a payload log. PII does not belong in a filter index, and neither do blob payloads.
Why `user.email` is two bugs in one line
The PII problem
Email addresses are personal data under GDPR, CCPA, HIPAA-adjacent rules, and almost every enterprise privacy policy. Putting them in a trace store does three bad things: it raises a compliance flag the moment security reviews the pipeline, it expands breach blast radius (a leaked trace export now contains user identities), and it limits where the trace store can live (data-residency rules apply).
The cardinality problem
Trace backends index attributes for fast filtering. Langfuse uses ClickHouse; Phoenix uses similar OLAP storage; LangSmith uses its own backend. All of them maintain indexes on attributes for sort, group-by, and filter operations. An unbounded-cardinality column (one distinct value per user) makes those indexes slower to build, slower to query, and more expensive to store. At 100,000 users the cost is small; at 10 million users it is real.
Why both bugs land on the same line
Email happens to be both PII and unbounded. The two problems travel together but are independent: a stable opaque user id solves both. Anonymous, low-cardinality (per-cohort, not per-user) attribution would solve cardinality but not PII; PII-cleaned but per-user email hashes solve PII but not cardinality. The right shape solves both: a stable opaque id (UUID or salted hash) set once per trace via the vendor's user-attribution API.
The right pattern
Most vendors expose a trace-level user attribution call: Langfuse's langfuse.update_current_trace(user_id=...), LangSmith's equivalent, Phoenix's user.id convention via OpenInference. Set the opaque id once on the trace root and skip the per-span attribute entirely. Per-user dashboards, support lookups, and cost attribution all work; you join against your user database in the application layer when you need to resolve the id.
Situations where this technique stops working.
2–4 min · Everything important, quickly.
Real products, models, and research that use this idea.
- Langfuse's user-attribution API sets a trace-level user_id specifically so per-user dashboards work without burning attribute cardinality.
- OpenInference and OpenLLMetry both emit prompts and messages as span events under the OTel GenAI conventions, not as attributes.
What an interviewer would ask next. Try answering before peeking at the approach.
QHow does the blob-pointer pattern work in practice, and what is the read-side workflow?
Write the full prompt and response to S3 (or equivalent) keyed by trace and span id. Store only prompt.blob_url (or a key) as a span attribute. The observability UI either resolves the link on demand when a developer opens the span, or a sidecar service materializes the blob into a structured view. Trace ingest stays cheap; full content stays accessible.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Defending the email attribute as 'we need it for support tickets'. Support lookups belong against your user table via an opaque id, not against the trace store's attribute index.
60 second bullets to scan on the way to the call.
Why high-cardinality attributes hurt trace-backend performance
Why PII does not belong in trace attributes
Primary sources. Browse if you want the original framing.
Same topic, related formats. Practice these next.