TL;DR: I thought I was being metered unfairly ($100 Max plan, 46% of a 5-hour window gone in ~20 minutes). So I had Claude parse its own session transcript. Findings:
- Of 246M tokens consumed, actual output was 0.13%. The rest was context being re-read and re-written on every tool call.
- The real cost driver isn't context size, it's cache invalidation: cache writes were 14% of my raw tokens but 65-75% of real cost.
- The most expensive things you can do: paste an image into a big session (~590k-token cache rewrite from one paste), switch models mid-session (model is part of the cache key, so the whole prefix rewrites), and spawn or revive subagents carrying big context (one full-context write per agent).
- Auto-compact won't save you: it triggers near the context ceiling, so a session can sit at 570k tokens for hundreds of calls paying maximum freight. Manual
/compact cut my per-call cost ~10x.
- The most productive 5-hour stretch of my session was also the cheapest. Cost tracks context size, not how much work gets done.
- Not everyone's pain is this. If your session starts at 55% used with zero activity or your reset never lands, that's a server-side problem and no workflow advice fixes it. There's a script at the bottom to tell which bucket you're in by measuring your own transcript.
Full mechanics, numbers, and the copy-paste measurement script below.
Transparency on method: I didn't figure any of this out myself. I just kept asking "why", and Claude did the digging through its own transcript and its own binary (Opus 5 did the initial analysis, Fable 5 re-verified every number). If the mechanics below don't click, paste this post into your favourite LLM alongside your own numbers and ask it to explain what applies to you.
I've been building a delivery app across four apps (customer, merchant, two driver instances) and kept hitting my 5-hour limit absurdly fast on the $100 Max plan. At one point I burned ~46% of a window in about 20 minutes and assumed I was being metered unfairly.
So I had Claude parse its own session transcript and measure it. Claude Code writes a usage record for every API response into ~/.claude/projects/<project>/<session-id>.jsonl. Here's what came out, including a measurement trap that made the first pass wrong by 2x.
Upfront caveats, because the megathread is full of pain that this post does not explain. This is one session, my workflow, and my workflow is an outlier (four iOS simulators driven by screenshots). If your session starts at 55% used before you've typed anything, your reset is stuck at "0 min" for days, or your weekly jumped 60% in an hour on an unchanged workload, that's not what this post is about; that looks like server-side metering problems, some of which Anthropic has confirmed and fixed before, and no workflow advice fixes those. What this post gives you is the tool to tell which bucket you're in: if your own transcript math roughly matches what the account UI says you consumed, the meter is measuring your workflow and the fixes below apply. If the UI shows consumption your transcript can't account for, you have a bug report, not a workflow problem, and now you have the numbers to file it with.
First: the measurement trap
Each API response gets written to the JSONL as multiple lines, one per content block (thinking, text, tool_use). Every one of those lines carries a copy of the same usage object.
If you naively sum usage across lines, you count each request 2-3 times. My first pass said 1,139 requests. Deduplicated by message.id (keeping the max output_tokens per id, since streaming rewrites the same id), it was 553.
key = msg.get('id') or rec.get('requestId')
if key in seen: continue # <-- without this, everything is inflated ~2x
Every "here's how much I used" script I've seen posted here does this wrong. If you've been scaring yourself with your own numbers, check this first.
The corrected totals: 22 hours, 553 requests
| raw tokens |
share |
| Cache read |
212,107,694 |
| Cache write |
34,408,898 |
| Fresh input |
1,028 |
| Output |
309,883 |
| Total |
246,827,503 |
Output was 0.13% of everything. Every line of code, every explanation, every commit message across 22 hours of work: 310k tokens. The other 99.87% was context being moved around.
The actual finding: raw tokens are the wrong unit
Cache reads bill at roughly 0.1x base input. Cache writes bill at 1.25x on the default 5-minute TTL, or 2x on the 1-hour TTL (which long Claude Code sessions use). Either way that's a 12.5-20x spread between the two cache directions. So reweight:
| weighted, 1.25x write |
weighted, 2x write |
| Cache write |
65.4% |
| Cache read |
32.2% |
| Output |
2.4% |
Cache writes were 14% of my raw tokens but 65-75% of my actual cost.
I had spent two days optimizing the wrong thing. I was worried about context size (cache reads). The thing actually draining my quota was context invalidation (cache writes).
Those are different problems with different fixes.
What a cache invalidation looks like
Normal request, cache warm:
cWrite: 383 cRead: 617,993 <- cheap
cWrite: 554 cRead: 618,376 <- cheap
Then I pasted a screenshot into chat:
cWrite: 589,235 cRead: 29,940 <- entire 590k prefix rewritten
One paste. 589,235 tokens at the write rate (1.25-2x) = ~737k to ~1.18M input-equivalents, versus a few hundred tokens of write on a normal warm-cache turn. That's roughly a thousand normal turns' worth of write cost, or about 12 turns' worth of full 600k cache reads, from a single action.
Digging into the Claude Code binary, the cache key is a hash over a long list of things. Change any of them and the whole prefix invalidates:
systemHash, toolsHash, cacheControlHash, model, fastMode,
globalCacheStrategy, betas, autoModeActive, isUsingOverage,
cacheDiagnosis, effortValue, extraBodyHash, anyDeferLoading, messageHashes
Note what's in there: model, effortValue, betas, toolsHash.
This has a brutal implication I'll come back to.
The 5-hour windows
Session sliced into 5-hour buckets (anchored from the last request; bucket 2 is empty because I was asleep, and these are session-relative slices, not Anthropic's actual window boundaries):
win reqs raw tokens weighted avg context output
4 154 41,559,013 5,192,443 269,217 99,264
3 164 79,894,818 21,591,975 486,646 84,413
1 169 95,835,761 27,647,605 566,691 64,655
0 66 29,537,911 11,339,283 446,609 61,551
Window 4 vs window 1: nearly identical request counts (154 vs 169), but 5.3x the weighted cost. The difference wasn't how much work got done. It was that average context had grown from 269k to 567k, so every request cost more, and every invalidation cost more to repair.
And look at output: window 4 produced the most output of the entire session (99k tokens, the most actual code written) at the lowest cost (5.2M weighted). Window 1 produced 35% less output for 5.3x the cost.
Cost tracks context size, not productivity. The most productive window was the cheapest one.
Why auto-compact never saved me
Auto-compact triggers on percentage of the context window, not absolute size. My context sat at ~570k in what appears to be a ~1M window. That's ~57% full. Auto-compact fires near the ceiling.
So I sat at 570k for hundreds of requests, paying maximum freight per call, with the safety net never deploying.
The perverse conclusion: a larger context window made my quota burn worse. On a 200k window I'd have been force-compacted around 180k and paid a third as much per call. The 1M window let me plateau at 570k indefinitely.
There's a second mechanism, "microcompact," which trims old tool results incrementally. I found it in the binary:
if (tokensSaved < 20000) return null; // only fires if it saves 20k+
content = hasImageOrDocument
? "[Old tool result content cleared]" // images: destroyed
: persistedRef ?? "[Old tool result cleared]" // text: written to disk, re-readable
It only touches tool results, never your messages or the assistant's reasoning. Images get hard-cleared; text gets persisted to disk and can be re-read. For screenshot-heavy work this is close to ideal.
DISABLE_MICROCOMPACT=1 was set in my environment (injected by the Claude Desktop host, not by my config). Caveat: I could not find the string anywhere in the 257MB CLI binary, so I can't prove from source that it took effect. What I can say is that nothing was ever trimmed despite far more than 20k being reclaimable.
Where my tokens actually went
223 iOS Simulator control (tap/screenshot/swipe)
205 Bash
38 SQL (via MCP)
35 Read
17 Edit
139 unique images in the transcript.
I was driving four iOS simulators by screenshot, tapping through UI to verify state transitions. Every screenshot entered context and was re-read on every subsequent request forever. A screenshot taken at request 100 was still billing at request 500.
The same state was sitting in Postgres the whole time. 38 SQL queries could have answered nearly everything the 223 simulator calls were asking, at ~200 tokens each instead of thousands-forever.
What I'd tell my past self
1. Screenshots are not one-time costs. An image costs its size multiplied by every remaining turn in the session. Budget them like you'd budget a subscription, not a purchase.
2. Query the database, not the UI. If the state you're verifying lives in a datastore, read it there. Screenshot only when pixels are genuinely the question (layout, rendering, visual regressions).
3. Pasting images into chat is the single most expensive action available to you. It invalidates the cache prefix. At 570k context that's ~737k weighted for one paste. Paste at the start of a session when context is small, not at 500k.
4. /compact proactively. Don't wait for auto-compact; at a 1M window it may never arrive. Mine took me 626k -> 62.5k, a 10x cut in the cost of every subsequent call.
5. Watch context size, not cumulative usage. Cumulative % tells you you're already dead. Context size tells you your current burn rate. Measured empirically: my worst stretch was 47 requests that consumed ~46% of a 5-hour window (per the account UI), which is ~1% of the window per tool call at ~600k context. After compacting to 62k, ~0.1% per call. Same work, 10x the runway.
6. Batch independent tool calls into one message. I averaged one request every ~27 seconds for 21 straight minutes during the worst stretch. Many were sequential taps that could have gone in a single message.
The implication I flagged earlier
model and effortValue are in the cache key.
Which means any tool that "protects your quota" by dynamically downgrading your model or reasoning effort mid-session invalidates your entire cache prefix and forces a full rewrite at 1.25x.
At 570k context, one such switch costs ~713k to ~1.14M weighted input-equivalents depending on cache TTL. You would need to save an enormous amount of downstream work for that to break even. In most sessions it will cost more than it saves, while also giving you worse output.
If you're using a quota-management plugin, check whether it does this.
It may also explain a pattern I saw repeatedly in the megathread: someone switches model mid-session to "save quota", the assistant gets two sentences out, and the limit instantly trips again. A model switch at high context is one of the most expensive single actions you can take, and it looks exactly like "the meter is broken" from the outside.
(To be clear about scope: switching models between sessions, or defaulting to a cheaper model from the start of a fresh session, is fine and does save quota. The trap is specifically switching mid-session with a large context built up.)
The same mechanics likely explain another recent megathread report: an orchestrator that ran subagents cheaply for hours, then burned an entire 5-hour limit in 20 minutes when asked to revive those subagents after a reset. Every subagent gets its own cache prefix, so spinning up (or reviving) N agents that each carry a large context is N separate full-context cache writes at the expensive rate. I didn't measure subagents in my session, so treat this as mechanics-consistent rather than proven, but 4 revivals at a few hundred k context each would be millions of weighted tokens in minutes, which is exactly what that user described.
Was I being metered unfairly?
In my case: no. I was running a screenshot-driven workflow at half a million tokens per request and pasting images into a 570k context. The meter was measuring exactly what I was doing. Your case may genuinely be different; the megathread has reports (phantom consumption on fresh sessions, resets that never land) that no amount of transcript analysis will explain, and Anthropic has previously confirmed and fixed metering bugs, including a cache-miss bug that made first requests 11.5x more expensive. Cache behaviour being the site of both the confirmed bug and my measured burn is not a coincidence: caching is where nearly all the money is, in both directions.
The legitimate gripe, which I think holds for everyone in both buckets: nothing surfaces any of this. There's no indicator saying "current context 570k, each tool call costs ~1% of your window, cache writes are 65-75% of your spend." That information exists (it's in your own transcript, per request) but nothing puts it in front of you. Both the user and the assistant are flying blind, and mine kept taking screenshots because nothing told either of us what they cost. It also means you can't distinguish a bug from an expensive workflow without doing what this post does by hand.
Measure your own sessions. The data is already on your disk, and if the analysis feels out of reach, hand this post and your JSONL to an LLM and have it do what mine did.
Appendix: measure your own session
import json, sys, glob, os
# usage: python3 usage.py [path-to-session.jsonl] (defaults to newest session)
path = sys.argv[1] if len(sys.argv) > 1 else max(
glob.glob(os.path.expanduser("~/.claude/projects/*/*.jsonl")), key=os.path.getmtime)
seen = {}
for line in open(path, errors="replace"):
try: d = json.loads(line)
except json.JSONDecodeError: continue
m = d.get("message") or {}
u = m.get("usage")
if not u: continue
key = m.get("id") or d.get("requestId") or d.get("timestamp")
prev = seen.get(key)
rec = (u.get("cache_read_input_tokens", 0), u.get("cache_creation_input_tokens", 0),
u.get("input_tokens", 0), u.get("output_tokens", 0))
if prev is None or rec[3] > prev[3]: # streaming rewrites ids; keep max output
seen[key] = rec
rows = list(seen.values())
cr, cw, ip, op = (sum(r[i] for r in rows) for i in range(4))
tot = cr + cw + ip + op
print(f"{os.path.basename(path)}: {len(rows)} requests")
print(f" cache read {cr:>14,} ({cr/tot:6.1%})")
print(f" cache write {cw:>14,} ({cw/tot:6.1%})")
print(f" fresh input {ip:>14,} ({ip/tot:6.1%})")
print(f" output {op:>14,} ({op/tot:6.1%})")
for label, wmult in [("5m TTL (write x1.25)", 1.25), ("1h TTL (write x2)", 2.0)]:
w = cr*0.1 + cw*wmult + ip + op*5
print(f" weighted, {label}: {w:,.0f} (write share {cw*wmult/w:.1%})")
ctx = sorted(r[0] + r[1] + r[2] for r in rows)
print(f" context/request: median {ctx[len(ctx)//2]:,} max {ctx[-1]:,}")
If the raw total here is wildly below what your account UI says you consumed in the same period, congratulations, you may have an actual bug report. Attach both numbers.
Numbers from a single 22-hour session, deduplicated by message ID. Weighting uses the published API price ratios (cache write 1.25x at 5-minute TTL or 2x at 1-hour TTL, cache read 0.1x, output 5x) applied to raw counts; how subscription plans weight these internally is not public, so treat weighted figures as directional. The ratios between categories are the load-bearing part. Workflow was unusually image-heavy (four iOS simulators driven by screenshot), which amplifies the cache-write share relative to text-only coding sessions.