11 September 2026

99.81% of what I paid Claude Code for was reading, not writing

I have been using Claude Code as my main way of writing software for about a year. Last week I finally sat down and measured what that actually cost, not in money, but in tokens, because tokens are the thing the money is calculated from.

60,163 requests. 24.08 billion input tokens. 46.4 million output tokens.

Which means 99.81% of every token I have paid for was input. Not code the model wrote. Context it read.

I expected that ratio to be lopsided. I did not expect it to be 518 to 1.

Where these numbers come from

Claude Code writes a JSONL file for every session under ~/.claude/projects/, and beside every assistant message it records exactly what that request cost. Not an estimate. The provider's own accounting, on your disk, right now.

You can see one for yourself:

grep -o '"usage":{[^}]*}' ~/.claude/projects/*/*.jsonl | head -1

Four fields matter. input_tokens is what you paid full price for. cache_read_input_tokens is what came out of the prompt cache at a discount. cache_creation_input_tokens is what got written into that cache at a premium. output_tokens is what the model generated.

One warning if you go counting these yourself, because it cost me an afternoon. The usage block also contains an iterations array that repeats every one of those numbers. If you scan the raw text rather than parsing the JSON and reading only the top level, you will get exactly double, and the result will look completely plausible. I shipped that bug and only caught it because I had a second implementation to disagree with the first.

What 518 to 1 actually means

Every request an agent makes carries your context with it. The system prompt, your CLAUDE.md, the files it has read, the tool output it has seen, and the entire conversation up to that point. Then it writes maybe a few hundred tokens back.

So the interesting question is not how much your agent writes. It is how much it has to re-read to write anything at all. On my numbers, output is a rounding error.

That reframes what is worth optimising. Shorter answers save you almost nothing. A tighter CLAUDE.md, fewer files pulled into context, and shorter sessions all hit the 99.81%.

The prompt cache is doing more work than I realised

Of those 24.08 billion input tokens, 23.28 billion were served from cache. That is a 96.7% cache hit rate, and every token written into the cache was read back 28.9 times on average.

Cache writes cost more than normal input and cache reads cost a fraction of it, so that multiple is the whole economics of a long agent session. Below about two reads per write the cache is costing you money. At 28.9 it is the only reason a 60,000 request year is affordable at all.

It also explains something that felt wrong when I first noticed it. Every cached token in my history sits on the one hour lifetime, and none on the five minute one. Long sessions with a stable prefix are exactly the shape prompt caching is built for, and an agent working through a task is the ideal case.

A quarter of the output was thinking

11.8 million of my 46.4 million output tokens were reasoning tokens. Around 25%.

I have no complaint about that. The reasoning is frequently the part worth reading, and it is what stops a wrong answer arriving confidently. But it is worth knowing that a quarter of what you pay on the output side is the model thinking rather than the model answering, because it is invisible in the transcript unless you go looking.

Measure your own

None of this generalises. It is one person, one year, mostly Rust and TypeScript, two models doing almost all the work. Your ratio will be different, and the point of writing it down is that yours is knowable and almost certainly unknown to you.

The rough version, in one command, for a single session file:

python3 - <<'EOF'
import json, glob, os
tot = {"in": 0, "read": 0, "write": 0, "out": 0}
for f in glob.glob(os.path.expanduser("~/.claude/projects/*/*.jsonl")):
    for line in open(f, errors="ignore"):
        try: u = (json.loads(line).get("message") or {}).get("usage")
        except Exception: continue
        if not u: continue
        tot["in"] += u.get("input_tokens") or 0
        tot["read"] += u.get("cache_read_input_tokens") or 0
        tot["write"] += u.get("cache_creation_input_tokens") or 0
        tot["out"] += u.get("output_tokens") or 0
served = tot["in"] + tot["read"] + tot["write"]
print(f"input {served:,}  output {tot['out']:,}")
print(f"input share {100*served/(served+tot['out']):.2f}%")
print(f"cache hit   {100*tot['read']/served:.1f}%")
print(f"ratio       {served/max(1,tot['out']):.0f}:1")
EOF

That reads the top level usage block only, so it avoids the doubling problem above.

There is also a page on this site that does the same thing without the terminal. It reads your projects folder in the browser, shows the same figures, and paints the result. Nothing is uploaded, because there is no server here that accepts a file.

Run it on your own history.

The thing I would tell myself a year ago

I assumed the cost of working this way was in what the model produced. It is not. It is in what the model has to hold in its head to produce anything, and that number is dominated by decisions I make. Which files it opens. How long a session runs. How much of my project I describe up front rather than letting it discover.

None of that was visible to me until I read my own transcripts. They had been sitting there the whole time.

All posts · Where each tool stores its history