Desktop usage card counts each API call about 2.2x (sums every transcript line, not each unique call)
Summary
The usage card on the Claude Code tab's home screen (Overview, "All") shows Total tokens: 135B. Counting each unique API call in the same local transcripts gives 62.2B. The card's number matches what you get by adding the message.usage of every transcript line. Claude Code writes one API response as several JSONL lines, one per content block (thinking, text, tool_use), and every line carries that response's full usage object. So each call gets counted once per content block.
Environment
- Claude desktop app 2.31226.1 (macOS 27.0.1), Code tab
- Claude Code CLI 2.1.278
- Transcripts:
~/.claude/projects/**/*.jsonl(1,265 files: 494 main sessions, 771 subagent files)
Steps to reproduce
- Use Claude Code over many sessions with tool use and extended thinking.
- Open the Code tab home screen, then go to Overview, All.
- Note "Total tokens".
- Run
usage_dedup_repro.py(full source at the bottom of this issue) against~/.claude/projects.
Expected
"Total tokens" counts each API call once, keyed by message.id plus requestId.
Actual
"Total tokens" equals the sum over every transcript line.
| | Every line | Each unique call |
|---|---|---|
| Records | 295,836 lines | 136,295 calls |
| input_tokens | 51.8M | 19.5M |
| output_tokens | 290.6M | 115.4M |
| cache_creation_input_tokens | 4.70B | 1.71B |
| cache_read_input_tokens | 130.98B | 60.33B |
| Total | 136.0B | 62.2B |
The card showed 135B. The line-level sum was 136.0B when measured, and a few sessions had run since the card was captured. The unique-call total is 62.2B, so the card overcounts by 2.19x. Session counts agree: the card shows 478 sessions, and the transcripts contain 474 unique sessionId values. That suggests the card is reading these same files.
The animation comparison follows the same inflated total ("~3461532x more tokens than Animal Farm").
Evidence that the extra lines are duplicates
- In the first content block of each line with usage, the type counts are tool_use 152,492, thinking 86,205, text 57,140 and fallback 3.
- 150,183 repeat lines have exactly the same
usageas an earlier line with the same(message.id, requestId). - 9,359 repeat lines differ, and only in
output_tokens, which never decreases in file order. These copies were written while the response was still streaming, so the final copy has the true count. - 11,687 calls appear in more than one transcript file. This looks like resumed or forked sessions copying earlier history. Deduplication has to run across all files, not file by file.
Suggested fix
Before summing, deduplicate usage records across all transcript files by (message.id, requestId). For each key, keep the record with the largest output_tokens. The same fix probably applies to the 30d/7d views, the per-model breakdown in the Models tab, and the activity heatmap if it is weighted by tokens.
Possibly related
A user remembers the card showing about 200-250M total around early September 2026. That fits an input-plus-output figure with the same per-line double counting (342.4M per line vs 134.9M per call today), which suggests the card started counting cache tokens at some point. This is inference; the card's earlier formula wasn't checked.
Repro script
usage_dedup_repro.py (full source below). It reads local transcripts only, writes nothing and has no dependencies.
python3 -I usage_dedup_repro.py ~/.claude/projects
Output on 2026-10-11 (a few more sessions had run than in the table above):
transcript lines with usage : 295,851
unique API calls : 136,303
calls found in >1 file : 11,687
unique sessionIds : 474
per line per call
input_tokens 51.8M 19.5M
output_tokens 290.6M 115.4M
cache_creation_input_tokens 4.70B 1.71B
cache_read_input_tokens 130.98B 60.33B
TOTAL 136.03B 62.17B
inflation 2.19x
<details>
<summary>usage_dedup_repro.py</summary>
#!/usr/bin/env python3
"""Compare Claude Code token totals: every transcript line vs each unique API call.
Run: python3 -I usage_dedup_repro.py [~/.claude/projects]
Reads local transcripts only. Writes nothing.
"""
import collections, glob, json, os, sys
FIELDS = ("input_tokens", "output_tokens",
"cache_creation_input_tokens", "cache_read_input_tokens")
root = os.path.expanduser(sys.argv[1] if len(sys.argv) > 1 else "~/.claude/projects")
naive = [0, 0, 0, 0]
lines = 0
calls = {} # (message.id, requestId) -> usage tuple
files_per_call = collections.defaultdict(set)
sessions = set()
for path in glob.glob(os.path.join(root, "**", "*.jsonl"), recursive=True):
with open(path, "rb") as fh:
for raw in fh:
if b'"usage"' not in raw:
continue
try:
rec = json.loads(raw)
except ValueError:
continue
msg = rec.get("message")
if not isinstance(msg, dict) or not isinstance(msg.get("usage"), dict):
continue
u = tuple(int(msg["usage"].get(f) or 0) for f in FIELDS)
lines += 1
naive = [a + b for a, b in zip(naive, u)]
if rec.get("sessionId"):
sessions.add(rec["sessionId"])
key = (msg.get("id"), rec.get("requestId"))
if key[0] is None:
key = ("no-id", rec.get("uuid"))
files_per_call[key].add(path)
prev = calls.get(key)
# Copies of one call differ only in output_tokens, which grows while
# the response streams. Keep the largest (final) count.
if prev is None or u[1] > prev[1]:
calls[key] = u
dedup = [sum(v[i] for v in calls.values()) for i in range(4)]
fmt = lambda n: f"{n/1e9:.2f}B" if n >= 1e9 else f"{n/1e6:.1f}M"
print(f"transcript lines with usage : {lines:,}")
print(f"unique API calls : {len(calls):,}")
print(f"calls found in >1 file : {sum(len(s) > 1 for s in files_per_call.values()):,}")
print(f"unique sessionIds : {len(sessions):,}")
print()
print(f"{'':30}{'per line':>12}{'per call':>12}")
for name, a, b in zip(FIELDS, naive, dedup):
print(f"{name:30}{fmt(a):>12}{fmt(b):>12}")
print(f"{'TOTAL':30}{fmt(sum(naive)):>12}{fmt(sum(dedup)):>12}")
print(f"{'inflation':30}{sum(naive) / max(sum(dedup), 1):>11.2f}x")
</details>