[BUG] OTel token.usage / cost.usage silently stop recording main-loop turns mid-session (3% of actual captured)

Status Open
Reported on v2.1.263
Maintainer reply None cached
Activity 0 comments · opened Sep 8, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Part-way through a long-lived session, Claude Code stops recording the developer's own turns to claude_code.token.usage, claude_code.cost.usage and claude_code.active_time.total. The usage is not written anywhere else — it is lost, and unrecoverable after the fact. Restarting the session clears the condition (and destroys the evidence).

It is silent: the exporter, transport and auth stay healthy, and in the telemetry an affected developer is indistinguishable from an idle one.

Measured on one session (53dd124d), against the session transcript as ground truth (transcript_path carries per-message usage; must be deduplicated by message.id, as the file repeats each message):

| run | actually consumed | recorded | captured |
|---|---|---|---|
| 4 Sep | 22,994,009 | 5,797,429 | 25% |
| 7 Sep | 1,508,313 | 1,508,313 | 100% |
| 8 Sep | 21,861,971 | 0 (main) | 0% |

The method validates itself on the clean 7 Sep run, matching the exported metric on every component — input 22, output 9,595, cacheRead 956,931, cacheCreation 541,765 — and cost to the fourth decimal ($4.1042 computed vs $4.1045 recorded). So the meter is exact when it works, and the shortfall is real loss rather than a measurement artefact. On the 8 Sep run, cost recorded $2.62 against roughly $25.98 incurred.

Only one recording path dies, not the meter. Over 4 hours in which main-loop token growth was 0, other instruments on the same session kept advancing: lines_of_code.count +1,021, code_edit_tool.decision +13, commit.count +2. Meanwhile token.usage, cost.usage and active_time.total stalled together (active_time.total pinned at 197.968 for 24h+). The exporter delivered 15 samples per 15 minutes throughout, so this is not export, transport, auth or temporality.

The onset is sharp, and the degradation is partial rather than total. The counter tracks the transcript to the unit, request for request, and then stops:

| transcript request | cumulative | metric query_source="main" |
|---|---|---|
| 02:38:30 | 3,637,625 | 3,637,625 |
| 02:39:32 | 4,436,272 | 4,436,272 |
| 02:39:59 | 4,717,604 | 4,717,604 |
| next 5 requests (755,887 tokens) | 5,473,491 | 4,877,909 — only 160,305, one request's worth |

After onset only five further credits occurred in 2.5 hours, and each matched exactly one request's token count (160,305 / 78,446 / 120,125+136,854 / 91,792 / 83,368). 50 of 207 requests were credited in full; 157 were dropped entirely.

Eliminated with evidence (so they aren't re-investigated):

| Cause | Ruled out by |
|---|---|
| Process restart / counter reset | No reset; session_id and service_version unchanged across the stall |
| Sleep / resume | Machine continuously awake from 3 Sep 23:48:45Z to 7 Sep 07:29:15Z per pmset -g log; the onset is mid-stretch |
| Idle duration | The 100%-capture run contains idle gaps of 880 s, 781 s and 759 s — longer than the 862 s gap at the onset |
| Compaction | First isCompactSummary was 16 minutes after onset |
| Auto-update | No version change in the window |
| Auth / transport | 15 samples per 15m throughout; no denials from this host |
| Query-source relabelling | Volume: 21.9M consumed against 730k in auxiliary |

Still unexplained, and our best remaining guess: something tied to process age or cumulative turn count. One process was exact for 50 requests / ~30 min then degraded; another was exact for 11 requests / ~51 min, then idled ~23 h and credited nothing thereafter.

What Should Happen?

Every API request's usage should be recorded to token.usage and cost.usage for the life of the session, as it is during the first ~30 minutes.

Failing that, the failure should be observable: a counter of turns that completed without being credited would make it detectable. Today it is indistinguishable from an idle developer, which is why it went unnoticed for a full working day and made per-user cost reporting unusable.

Error Messages/Logs

# No errors are emitted. The failure is silent.
# Exporter health during the stall (60s interval, OTLP http/protobuf):
#   max(count_over_time(claude_code_token_usage_tokens_total[15m])) = 15
# Same session, same 4h window:
#   claude_code_token_usage_tokens_total{query_source="main"}   growth: 0
#   claude_code_lines_of_code_count_total                       growth: +1021
#   claude_code_commit_count_total                              growth: +2
#   claude_code_active_time_seconds_total                       pinned at 197.968

Steps to Reproduce

  1. Enable OTel metrics export (CLAUDE_CODE_ENABLE_TELEMETRY=1, OTLP http/protobuf, cumulative temporality, 60s interval).
  2. Work continuously in one session for more than ~30 minutes, with tool-heavy turns (many API requests per user turn).
  3. Compare claude_code.token.usage for that session_id against the session transcript, summing message.usage deduplicated by message.id.
  4. The metric matches exactly for the first stretch, then diverges permanently. It does not recover until the session is restarted.

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.263 (macOS arm64, BUILD_TIME 2026-09-06T01:08:56Z, GIT_SHA 37ae3f38d765199d54a6913cd61c6c9ad8576cc6)

Platform

AWS Bedrock

Operating System

macOS

Terminal/Shell

iTerm2

Additional Information

  1. What can stop the crediting path being reached for main-loop turns mid-session? In the shipped bundle, cost and all four token types are recorded by one function guarded by a per-turn latch (if (Gp !== null && ty !== "credited") { ty = "credited"; ... }, plus a message_stop case for "pending"). Can that latch persist as "credited" across turns, or the usage object stay null, in a long-lived REPL session? The partial-credit pattern above — exactly one request's worth surviving per burst — looks like a per-turn accumulator being lost rather than a counter being detached.
  2. Why do token.usage, cost.usage and active_time.total stall together while lines_of_code.count and commit.count keep working? Whatever those three share and the others do not should localise it quickly.
  3. How can we capture diagnostics from an already-affected process without restarting it? A restart clears the condition, so "restart Claude Code" destroys the only live evidence. OTEL_LOG_LEVEL=debug cannot be enabled retroactively.

Possibly related: #33904 reports a token-usage surge alongside session-recording problems, though on Windows/VS Code and without measured figures, so I have filed this separately rather than commenting there.

Happy to supply the transcript-versus-metric diffing script, or raw Prometheus series, on request.

View original on GitHub ↗