
You open the dashboard, pick the Claude Code token usage chart, and see a flat line. Maybe it is empty. Maybe it sits at zero while you know you burned through a long refactor an hour ago. The instinct is to change environment variables at random, restart the shell, and hope.
Don't. An empty or flat chart has a small number of causes, and they fall into two very different buckets. Either no data arrived at your backend, which is a pipeline problem, or data arrived and the value really is zero, or looks like zero because of how you queried it. Those two buckets need different fixes, and mixing them up wastes an afternoon.
This guide walks five checks in a fixed order, cheapest first. It is a diagnostic, not a setup tutorial. If you have not yet wired Claude Code to an OpenTelemetry backend, start with the setup guide in the telemetry.dev Claude Code integration docs and come back here when the chart misbehaves.
Missing vs zero: why the distinction decides your debugging path
A missing series and a zero-valued series look nearly identical on a chart. On the wire they are completely different events.
Missing means your backend never received a data point for that metric. Nothing was exported, or it was exported somewhere you are not looking, or it was rejected on the way in. The fix lives in configuration, timing, or network.
Zero means a data point (or a query result) with a value of 0 exists. That can be real: a session that made no model calls produces no tokens. It can also be an artifact: a filter that matches nothing, a rate calculation over an empty window, or a chart that renders "no data" as a flat baseline. The fix lives in the query.
Settle which bucket you are in first, and you can do it without touching Claude Code at all. Go to your backend's raw data view (not the chart) and look for any claude_code.token.usage data point in the last hour, with no filters applied. Found one? You are in the zero bucket, so skip to Check 5. Found nothing? You are in the missing bucket, so start at Check 1.
Here is the position this guide takes: don't trust a chart to tell you whether data exists. Charts smooth, bucket, and default. Raw points don't. Every minute you spend staring at a dashboard panel before looking at raw data is wasted.
One more framing note. A raw view that shows a handful of points is a meaningful result, not a failure. A few points per session is normal for metrics exported on an interval.
Check 1: Is telemetry enabled and is a metrics exporter selected?
Claude Code does not export telemetry by default. Two settings gate everything, and a surprising number of empty charts come down to one of them being absent in the shell that actually launched the session.
First, CLAUDE_CODE_ENABLE_TELEMETRY must be set to 1. Without it, nothing downstream matters. Second, OTEL_METRICS_EXPORTER must name an exporter. For an OTLP backend that is otlp. If it is unset or set to none, Claude Code collects nothing it can send.
The common failure is not forgetting the variables. It is setting them in the wrong place. You exported them in one terminal tab and launched Claude Code from another. You put them in a shell profile that your IDE-integrated terminal does not source. You set them in a project .env file that Claude Code never reads. Environment variables belong to a process, and a long-running session keeps the environment it started with.
Verify from inside the same environment that runs Claude Code:
Run env | grep -E 'CLAUDE_CODE|OTEL' in the exact terminal you launch from.
Confirm CLAUDE_CODE_ENABLE_TELEMETRY=1 and OTEL_METRICS_EXPORTER=otlp both appear.
Start a fresh Claude Code session after any change. A running session won't pick up new variables.
If you manage settings centrally or through a config file, check that nothing there overrides the shell values. A managed value can silently win over what you typed.
This check is cheap and it is the most common culprit. If both variables are present and you started a new session, move on. If you fixed something here, wait out the export interval covered next before declaring victory. The fix can be correct and the chart can still look empty for a minute or more.
Check 2: Is it just export timing?
Metrics are not streamed. They are batched and flushed on a timer, and the default timer is slow enough to fool you.
OTEL_METRIC_EXPORT_INTERVAL defaults to 60,000 ms. That is one flush per minute. OTEL_LOGS_EXPORT_INTERVAL defaults to 5,000 ms, so log and event records leave roughly twelve times more often. If you send a prompt, glance at the metrics chart three seconds later, and see nothing, that is expected behavior, not a bug.
Short sessions make this worse. Say you launch Claude Code, ask one question, read the answer, and quit in 40 seconds. The metric interval may never fire before the process exits. Depending on how the process shuts down, the final batch may not flush at all. You did real work and tokens were consumed, but no metric data point ever left the machine.
The practical test is simple. Run a session for at least two full export intervals. Ask for several tasks, keep working for two to three minutes, then check the raw data view. If points appear, timing was your problem and nothing is broken.
While debugging, shorten the interval so you aren't waiting on a minute-long timer. Setting OTEL_METRIC_EXPORT_INTERVAL to something like 10000 (10 seconds) gives you fast feedback. Treat that as a diagnostic setting. A 10-second interval produces more data points per session, and since each metric data point counts toward your ingestion units, put it back to the default once the pipeline is confirmed.
One trap: people lower the logs interval and assume it speeds up metrics. They are separate settings with separate defaults. Change the one that matches the signal you are staring at.
Check 3: Metrics vs logs/events pipelines
Claude Code emits two different kinds of signal, and they travel on two different pipelines. This check explains the most confusing symptom of all: one chart has data and the other is empty, with the same session behind both.
The aggregate counter is the metric claude_code.token.usage. It travels on the metrics pipeline, selected by OTEL_METRICS_EXPORTER, flushed on the 60-second interval. The per-request detail comes as events, such as claude_code.api_request, on the logs pipeline, selected separately and flushed every 5 seconds by default.
So the failure matrix is real. You can have the logs exporter configured and the metrics exporter missing. Then every claude_code.api_request event shows up within seconds, and the token usage metric never appears. The reverse also happens: metrics flow, but the logs exporter was never selected, so per-request events are absent.
The stance here: when one signal works and the other doesn't, stop suspecting the endpoint. If events reach your backend, the network path, protocol, and auth are proven good, because they are shared. The missing signal's exporter was almost certainly never turned on. Go back to Check 1 and look at the specific exporter variable for the signal that is empty.
It also changes how you cross-check numbers. If the metric chart looks suspiciously low, you can sum token fields across claude_code.api_request events for the same window and compare. They won't match perfectly. The metric is an aggregate flushed on a timer and the events are individual records, so a short window will diverge on boundaries. But a 10x gap tells you something is wrong, and a rough match tells you the metric is healthy.
Backends that accept standard OTLP/HTTP, as telemetry.dev does, receive both signals through the same endpoint, so one working signal is strong evidence about the shared transport.
Check 4: Endpoint, protocol and auth
If both exporters are selected and you have waited out the intervals, the next question is whether the OTLP request physically reached your backend and was accepted. Silent failures hide here, because exporters generally log and move on rather than crashing your coding session.
Three things have to line up.
Endpoint. OTEL_EXPORTER_OTLP_ENDPOINT must point at your backend's OTLP/HTTP ingest address. A typo, a stale URL from a previous experiment, or a localhost collector address that is not running all produce the same result: data leaves the process and goes nowhere.
Protocol. OTEL_EXPORTER_OTLP_PROTOCOL has to match what the receiver speaks. telemetry.dev accepts OTLP/HTTP, so a gRPC setting pointed at an HTTP endpoint will fail quietly. Match them deliberately.
Auth. Most backends require a header carrying an API key, passed through OTEL_EXPORTER_OTLP_HEADERS. A missing or wrong key typically earns a 401 or 403 response. The classic mistake is a stray space, a missing prefix, or quoting that the shell mangles, so the header the server sees is not the header you meant.
If you route through an OpenTelemetry Collector, the collector adds a second place for config to go wrong, including how secrets are injected. The post on OpenTelemetry Collector environment variables for auth covers the pattern that avoids hardcoding keys and the failure modes when a variable does not expand.
To confirm delivery without guessing, test the path directly. Send a minimal OTLP request to the same endpoint with the same headers using curl and read the HTTP status code. A 2xx means the endpoint and auth are fine. A 4xx names the problem. A connection error means the endpoint is unreachable from where Claude Code runs, which matters if you are inside a container, devcontainer, or remote machine where localhost means something different.
Also check limits on your side. On the free plan (30k units), a workspace that has burned through its allowance can look like a dead pipeline. Check your usage before assuming a config fault.
Check 5: Querying by attributes
You looked at raw data in the first section, found points, and still get a flat line on the chart. Now the problem is the query, and the data is fine.
Claude Code token usage is not one number. The claude_code.token.usage metric carries attributes that split it into separate series. The ones that bite most often are the token type (input, output, cache read, cache creation) and the model. A chart that filters to type=output on a model you did not use returns nothing. A chart summing only one token type looks low, or zero, in a session dominated by cached input.
Run through these in order:
Remove every filter and group-by. Look at the total across all attributes. If it is nonzero, a filter was hiding your data.
Check the token type. Cache-heavy sessions can show small output counts and large cache-read counts. Pick the type that answers your question, or sum across types on purpose.
Check the model attribute. If you switched models mid-project, a filter on the old model name shows a series that went flat on the day you switched.
Check how the metric is aggregated. claude_code.token.usage is a counter. Charting a raw cumulative counter, charting a rate, and charting a per-interval delta give very different pictures. A rate over a window with a single data point can come out as zero or empty because there is nothing to compute a slope from.
Check the time range and bucket size. With a 60-second export interval, a bucket narrower than a minute often contains no point at all, which renders as a gap or zero.
The position: a flat line from a filtered query is a query bug until proven otherwise. Don't touch your Claude Code configuration to fix a chart problem. Widen the query first, then narrow it one attribute at a time until the line disappears. The attribute that kills it is your answer.
If you want one view that avoids most of this, group by token type and model with no other filters, and read the series individually.
A short decision table and what these checks can't tell you
Match your symptom to the first check worth running.
| Symptom | Likely bucket | Start at |
|---|---|---|
| No data points in raw view, ever | Missing | Check 1 |
| Data appears after a few minutes, not instantly | Timing | Check 2 |
| Events present, token metric absent | Wrong exporter for one signal | Check 3 |
| Nothing arrives, exporters selected | Transport or auth | Check 4 |
| Raw points exist, chart is flat | Query | Check 5 |
| Numbers look low but nonzero | Query or token type | Check 5 |
Now the limits, because a diagnostic guide that overpromises is worse than none.
These checks tell you whether the pipeline works. They don't guarantee that every token Claude Code consumed was captured. Metrics flush on a timer, sessions can end before a flush, and a crash or killed process can drop the last batch. Treat the metric as a good signal, not an audit log.
Cost carries the same caveat. A cost figure depends on the token usage that was actually reported and on matching the model to a pricing entry. If usage was not reported, or the model does not match, the computed number will be missing or off. Don't reconcile it against an invoice and expect exact agreement.
If you still have a flat chart after five checks, you have narrowed the problem to a single layer, which is most of the work. For first-time wiring, the telemetry.dev Claude Code integration docs cover the setup path end to end, and the guide to tracing Vercel AI SDK calls with OpenTelemetry shows the same raw-data-first habit applied to another stack.
Conclusion
An empty Claude Code token usage chart is rarely mysterious. It is one of five things, and you can rule them out in about fifteen minutes if you check raw data first and work the list in order.
Make that your standing practice: when a chart looks wrong, look at raw points before you touch a single environment variable.
If you don't have a backend that makes raw points easy to inspect, create a free workspace at telemetry.dev (no credit card, 30,000 ingestion units) and point your Claude Code OTLP export at it using the Claude Code setup guide. Then run one two-minute session and look at the raw claude_code.token.usage points before you build any dashboards. If they are there, you have a working pipeline and a query problem. If they are not, you know exactly which of the first four checks to run.
Share this article
