Rajtík's blog

← Back to home

AI tools 9 min read

I wrote 192 tokens out of ten million

I ran a token usage audit on my Claude Code. Cache read is 96% and my own input is 0.002%. The script that measures it is in the article.

When I wrote the article about my new Claude Code plugin pis-cesky here in August, that session burned 9,969,294 tokens. I typed exactly 192 of them myself. That is less than two thousandths of a percent.

Those numbers are not an estimate. They sit in a log on your disk, and in a moment I will show you how to pull them out too.

The trigger was a video about token usage in Claude Code. The author claims his limits are not eaten by what he types but by everything that piles up around it, and he attaches an audit prompt as proof. I ran it on my own machine, because someone else's numbers are no use to me.

Why it grows like this

The model has no memory. Every API request carries the whole conversation from the start, otherwise it would not know what you are talking about. A file read in step four of a forty-step session is therefore paid for thirty-seven times.

The only reason this does not ruin you is that the conversation prefix is cached. Reading from the cache costs roughly a tenth of the regular input price, writing costs 1.25 times that with a five-minute TTL and double with a one-hour TTL. That gives you the break-even point: with the five-minute cache it pays off from the second request, with the one-hour cache from the third.

In my log it looks like this:

item tokens share
cache_read_input_tokens 9,575,016 96.05%
cache_creation_input_tokens 325,606 3.27%
output_tokens 68,480 0.69%
input_tokens 192 0.0019%

During that session the context grew from 49,201 tokens to 133,649. I did nothing big, just wrote one article and kept fixing it as I went.

Measure it yourself

Claude Code logs every session to ~/.claude/projects/<project>/*.jsonl, one JSON object per line, and the lines with a model response carry a usage object. All you have to do is sum it across the lines:

import json, sys, os

for path in sys.argv[1:]:
    tot = {'cache_read_input_tokens': 0, 'cache_creation_input_tokens': 0,
           'input_tokens': 0, 'output_tokens': 0}
    ctx = []
    with open(path) as fh:
        for line in fh:
            try:
                rec = json.loads(line)
            except ValueError:
                continue
            usage = (rec.get('message') or {}).get('usage')
            if not isinstance(usage, dict):
                continue
            for key in tot:
                tot[key] += usage.get(key, 0) or 0
            ctx.append(sum(usage.get(k, 0) or 0 for k in
                           ('cache_read_input_tokens',
                            'cache_creation_input_tokens', 'input_tokens')))
    total = sum(tot.values())
    if not total:
        continue
    print(f"\n### {os.path.basename(path)} ({len(ctx)} turns)")
    for key, value in sorted(tot.items(), key=lambda x: -x[1]):
        print(f"  {key:32} {value:>12,}  {100 * value / total:5.2f} %")
    print(f"  context: first turn {ctx[0]:,}, last {ctx[-1]:,}")

Run it on specific files:

python3 tokens.py ~/.claude/projects/-Users-you-project/*.jsonl

You get your own number instead of mine. The differences between setups are big, so this is the only part of the article I really recommend doing before you reconfigure anything.

My sample has its limits. Both sessions I measured (the August one and an older one) were about writing text, not reading someone else's code, and cache read came out at 96.05% and 94.61%. Work where an agent reads hundreds of files may look different.

What the audit found on my machine

The prompt from the video description goes through seven areas: memory files, MCP servers, model, hooks, subagents, scheduled tasks and cache. I put the whole thing at the end of the article.

The strongest finding: a globally connected MCP server godot-ai with about fifty tools, and it was loading even into a Laravel project that has nothing to do with the Godot engine. Next to it playwright and the project's laravel-boost.

The instruction files come to 21 KB: CLAUDE.md has 11,785 bytes, AGENTS.md another 9,464. That gets loaded into every session.

The scheduled tasks trap, which the video calls the worst one, does not affect me at all. My crontab -l is empty. And I have no proxy or gateway configured, so deferred tool loading works as it should.

I have three hooks, but none of them trims command output. When I run tests, the whole printout drops into the conversation and rides along until the end of the session.

Which advice holds up

Switching models in the middle of a session really hurts, and it is the least intuitive thing in the whole topic. The cache is tied to the model. You switch from Opus to Sonnet because you are running out of limit, and the whole history so far is recomputed at full price instead of a tenth. You wanted to save, and you pay more. Changing the effort level does the same - it always invalidates the message cache.

Compact is in a similar position. For the model to write you a summary of the conversation, it has to read the whole conversation once more. You are buying continuity, not savings. /clear is free, because it sends nothing.

What does not sit right with me

The video says that turning tools on and off during a session costs nothing, because they are supposedly just appended at the end. Anthropic's prompt caching documentation says otherwise: tool definitions sit at the start of the prompt, and adding, removing or reordering them invalidates the whole cache. Apart from a model change, that kind of tool change is the only thing that throws away the tools and system prompt cache too, not just the message history.

So I measured it. I went through all five sessions I have in that project - including the two the percentages above come from - 223 model responses in total, and looked for a turn where cache_read_input_tokens drops below the previous value. I did not find a single one. The prefix kept growing every time and cache_creation only added what was new.

I tried to force a rebuild on purpose, too. Loading a whole skill into a running session wrote 41,336 tokens, and cache_read on that same turn stayed at 97,178 - the content was appended to the end, the prefix was not read again. Loading the schema of a deferred tool turned out the same, cache_read continued from 173,670 to 175,100.

So both claims hold. The documentation talks about the tools field in the API request, where tools really do sit at the start. Claude Code with deferred loading apparently does not touch that field every time a new tool shows up, but materializes it at the end of the prompt, which the cache does not reach.

One thing I did not verify: connecting and disconnecting an MCP server itself through /mcp. For that I would have to touch the configuration and restart the session, which would destroy the state I was measuring. So on this step I still do not know.

The second thing I would not take literally: /usage supposedly names the specific skill, tool or agent that ate your limit. In reality it only shows plan usage. Do not look for a per-tool breakdown there.

The audit prompt

This text is from the video description, I am leaving it in the original as it is:

Audit this setup for token waste. Do not fix anything. Report only.

Use your shell and file tools to measure each item. If you cannot invoke slash
commands yourself, ask me to run /context and /usage and paste the output, then
continue.

1. MEMORY
   Find every CLAUDE.md in scope: this project, parent directories, the user
   level one, and anything pulled in with @imports. Report each file's size in
   tokens. Flag any single file over 5k and any total over 10k.

2. TOOLS
   List connected MCP servers and how many tools each exposes. State plainly
   whether tool deferral is ACTIVE or NOT. Then check for a proxy or gateway
   (ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, any gateway variable) and say so
   loudly if you find one, because routing through a proxy silently turns
   deferral off and nothing warns you.

3. MODEL
   Report the current model and effort level and where each is set. Flag any
   mode that changes model automatically during a session, because every
   switch rebuilds the whole cache.

4. HOOKS
   List any PreToolUse hooks that rewrite noisy commands to produce less
   output. If there are none, say so, because unfiltered test and build output
   lands in context verbatim and is re-sent for the rest of the session.

5. SUBAGENTS
   List every agent file in the project and user agent directories. For each,
   report whether it sets an explicit model in frontmatter or inherits the
   main session's model.

6. SCHEDULED WORK
   List every cron, scheduled task and background job with its interval.
   Compare each interval against the prompt cache lifetime. Flag every one
   whose interval is longer, because those miss cache on every single fire.

7. CACHE
   Parse the newest session log under the projects directory. For every
   assistant turn, sum usage.cache_read_input_tokens,
   cache_creation_input_tokens, input_tokens and output_tokens. Report each as
   a percentage of the total. Also report the context size on the first turn
   and on the last turn.

Output one table, sorted by cost, highest first:

   FINDING | SEVERITY | EVIDENCE | WHAT IT IS COSTING ME

Severity is RED, AMBER or GREEN. Evidence is a number or a file path, never an
adjective.

Then one final line: the single highest-leverage change I should make. One
line, nothing else.

Rules: measure, do not estimate. Write UNKNOWN rather than guessing. Change no
file and no setting.

Feel free to handle item 7 with the script above, the agent does not need to read a half-megabyte log into its context for that.

I am off to fix that Godot finding. I hope this article and the video help you the way they helped me 👍