A real Claude Opus 5.5 agent log: 97.5% of prompt tokens were cache hits, so 35 calls came to $2.39 at list prices instead of $22.08. A 101-line script checks your own logs, and shows why a broken cache can cost nearly double.
Without prompt caching, the Claude agent session I measured would have cost 9.2x as much at list prices: $22.08 instead of $2.39, for exactly the same tokens. Below is a script that reads your own Claude Code logs and tells you your cache hit rate, what it saves you, and what it would cost if it broke.
On November 4 the AI Engineer YouTube channel posted “Without Caching, Your AI Agent Costs 10x More”, a 20-minute talk by George Cameron, co-founder of the benchmarking firm Artificial Analysis.
Paraphrasing the captions: on their AA Briefcase benchmark, Claude Opus 5.5 at max effort averaged about $21 per task, and the task he walks through took 165 steps. The cost drivers he names are the number of steps (which he calls the biggest), input tokens per call, and the share of those that are cache hits. With zero cache hits, he said, that $21 task would cost over $200.
An agent re-sends its whole history on every step: system prompt, tools, every earlier tool result.
Prompt caching keeps that prefix: you pay extra to write it once, then a fraction to read it back. Anthropic’s pricing page, read today:
+---------------------+---------------+| Claude Opus 5.5 | $ per million |+=====================+===============+| Input (uncached) | $4.00 || Output | $20.00 || Cache write, 5 min | $5.00 || Cache write, 1 hour | $8.00 || Cache hit | $0.20 |+---------------------+---------------+
A cache hit costs one-twentieth of a normal input token. The talk cites a roughly 10x discount on cache-hit input; for Opus 5.5 it is 20x. Input is most of an agent’s tokens, so the hit rate decides most of the bill.
Claude Code writes every session to ~/.claude/projects/<folder>/<session>.jsonl. Each assistant line carries the API's usage object, with three input buckets: input_tokens (uncached), cache_creation_input_tokens (written) and cache_read_input_tokens (hits), plus a cache_creation object that splits the writes into 5-minute and 1-hour.
One API call can be logged on several lines, so the script counts each message ID once. A 1-hour write costs 1.6x a 5-minute one, so the split matters.
Standard library only, Python 3.8+. Edit PRICES for other models.
"""cachecheck.py - is prompt caching working in your Claude agent?Reads Claude Code / Agent SDK session logs (*.jsonl) and prices everycall twice: at list price, and as if no token had been a cache hit.usage: python3 cachecheck.py [file-or-dir ...] (default ~/.claude/projects)"""import json, sysfrom pathlib import Path# $ per million tokens: input, output, 5m write, 1h write, cache hit.# Basis: platform.claude.com/docs/en/about-claude/pricing, read 2026-11-04.PRICES = { "claude-fable-5-1": (10.00, 50.00, 12.50, 20.00, 0.25), "claude-opus-5-5": (4.00, 20.00, 5.00, 8.00, 0.20), "claude-sonnet-5-5": (2.00, 10.00, 2.50, 4.00, 0.10), "claude-haiku-5-5": (0.10, 0.50, 0.125, 0.20, 0.01),}def price_for(model): for name, p in PRICES.items(): if model.startswith(name): return p return Nonedef calls(files): seen = set() for f in files: for line in open(f, encoding="utf-8", errors="replace"): try: rec = json.loads(line) except ValueError: continue msg = rec.get("message") if isinstance(rec, dict) else None if not isinstance(msg, dict) or not isinstance(msg.get("usage"), dict): continue mid = msg.get("id") if mid is not None and mid in seen: # logged once per block continue seen.add(mid) yield f, str(msg.get("model")), msg["usage"]def main(args): roots = [Path(a) for a in args] or [Path.home() / ".claude/projects"] files = [] for r in roots: if not r.exists(): sys.exit(f"usage: cachecheck.py [file-or-dir ...]\nnot found: {r}") files += sorted(r.rglob("*.jsonl")) if r.is_dir() else [r] tot = dict(n=0, inp=0, w5=0, w1=0, hit=0, out=0, cost=0.0, nocache=0.0, hitcost=0.0, rewrite=0.0) short, skipped, last, count = [], set(), {}, {} for f, model, u in calls(files): p = price_for(model) if p is None: skipped.add(model) continue cc = u.get("cache_creation") cc = cc if isinstance(cc, dict) else {} w1 = cc.get("ephemeral_1h_input_tokens") or 0 w5 = (u.get("cache_creation_input_tokens") or 0) - w1 inp, hit = u.get("input_tokens") or 0, u.get("cache_read_input_tokens") or 0 out = u.get("output_tokens") or 0 cost = (inp*p[0] + out*p[1] + w5*p[2] + w1*p[3] + hit*p[4]) / 1e6 nocache = ((inp + w5 + w1 + hit)*p[0] + out*p[1]) / 1e6 key = (f, model) count[key] = count.get(key, 0) + 1 if key in last and hit < last[key]: # read less than last call cached short.append((Path(f).name, count[key], last[key] - hit)) last[key] = hit + w5 + w1 for k, v in zip(("inp", "w5", "w1", "hit", "out", "cost", "nocache"), (inp, w5, w1, hit, out, cost, nocache)): tot[k] += v tot["n"] += 1 tot["hitcost"] += hit * p[4] / 1e6 # a lost hit is written again at the TTL this call used tot["rewrite"] += hit * (p[3] if w1 > w5 else p[2]) / 1e6 prompt = tot["inp"] + tot["w5"] + tot["w1"] + tot["hit"] if not prompt or not tot["cost"]: names = ", ".join(sorted(skipped)) or "none" sys.exit(f"no priced calls with tokens (skipped: {names})") print(f"files {len(files)} API calls {tot['n']} (log: usage per call)") print(f"prompt tokens {prompt:>12,}") print(f" cache hits {tot['hit']:>12,} {tot['hit']/prompt:6.1%}") print(f" cache writes {tot['w5'] + tot['w1']:>12,} " f"{(tot['w5'] + tot['w1'])/prompt:6.1%} (1h: {tot['w1']:,})") print(f" uncached {tot['inp']:>12,} {tot['inp']/prompt:6.1%}") print(f"output tokens {tot['out']:>12,}") print(f"calls that read less than the previous call cached: {len(short)}") for name, n, gap in short[:5]: where = f"{name} " if len(files) > 1 else "" print(f" {where}call {n}: {gap:,} cached tokens not read back") print(f"cost at list prices ${tot['cost']:9.2f}") print(f"cost with no cache ${tot['nocache']:9.2f} (same tokens, 0 hits)") print(f"caching saved {1 - tot['cost']/tot['nocache']:.0%}; " f"no cache = {tot['nocache']/tot['cost']:.1f}x the cost") h = tot["hit"] / prompt print("what-if: hits fall, lost hits are re-written (same tokens)") for target in [h] + [t for t in (0.9, 0.8, 0.5, 0.0) if t < h]: lost = 1 - target / h if h else 0 c = tot["cost"] + lost * (tot["rewrite"] - tot["hitcost"]) print(f" hit rate {target:6.1%} -> ${c:8.2f} " f"({c / tot['nocache']:.2f}x the no-cache cost)") if skipped: print(f"skipped models with no price: {', '.join(sorted(skipped))}")if __name__ == "__main__": main(sys.argv[1:])
python3 cachecheck.py # every session on this machinepython3 cachecheck.py path/to/one.jsonl
A bad path gets a usage line and exit code 1.
I ran it on the Claude Code log of the scheduled Claude session that built this article, frozen at 35 API calls, all Claude Opus 5.5:
files 1 API calls 35 (log: usage per call)prompt tokens 5,458,781 cache hits 5,323,005 97.5% cache writes 135,702 2.5% (1h: 135,702) uncached 74 0.0%output tokens 12,091calls that read less than the previous call cached: 1 call 2: 17,020 cached tokens not read backcost at list prices $ 2.39cost with no cache $ 22.08 (same tokens, 0 hits)caching saved 89%; no cache = 9.2x the costwhat-if: hits fall, lost hits are re-written (same tokens) hit rate 97.5% -> $ 2.39 (0.11x the no-cache cost) hit rate 90.0% -> $ 5.59 (0.25x the no-cache cost) hit rate 80.0% -> $ 9.85 (0.45x the no-cache cost) hit rate 50.0% -> $ 22.62 (1.02x the no-cache cost) hit rate 0.0% -> $ 43.91 (1.99x the no-cache cost)
97.5% of 5.46 million prompt tokens were cache hits, and only 74 tokens were sent uncached. At list prices that is $2.39 versus $22.08 with no caching: 9.2x, or an 89% saving.
On call 2, 17,020 tokens that call 1 had cached were not read back. The log doesn’t say why; the script just flags it, which is your cue to look.
The what-if rows keep the same tokens and move hits into writes, at the TTL each call used.
At 80% hits the cost is still under half the no-cache price. With these 1-hour writes it breaks even just above 50%. At 0% it is 1.99x the no-cache cost, because every one of those tokens would be a 1-hour write at $8 per million, twice the plain $4 input price, and never read back.
This session used 1-hour writes on every call. On a copy with every write relabelled as 5-minute, the last what-if row reads:
hit rate 0.0% -> $ 27.54 (1.25x the no-cache cost)
Cheaper writes, smaller penalty: 1.25x at 0% hits. Either way, “caching is on” is not the same as “caching works”. If something early in the prompt changes every call, such as a timestamp in the system prompt, everything after it is written and paid for on every request and never read.
A hard-coded result would look identical, so I mutated the input. I rewrote the same log with every cached token moved into input_tokens: the script then reports 0 hits, $22.08 both ways and "1.0x the cost". Same tokens, no hits, so the two costs meet. Then a bad path: python3 cachecheck.py nope prints the usage line and exits 1.
In my log, every call records "promptCacheRequested": "1h" from Claude Code. Your own agent on the Messages API does not cache unless you ask: the docs say you opt in with cache_control. The simplest form is automatic caching, one field at the top level of the request:
response = client.messages.create( model="claude-opus-5-5", max_tokens=1024, cache_control={"type": "ephemeral"}, system=SYSTEM, messages=history,)
The default lifetime is 5 minutes; {"type": "ephemeral", "ttl": "1h"} buys an hour at 2x the input price. Pick 1 hour only if your agent s longer than 5 minutes between steps.
To use the script on your own agent, log each response as one JSONL line shaped {"message": {"id": ..., "model": ..., "usage": ...}}.
The docs say the cache follows the order tools, then system, then messages, and a change at one level invalidates that level and everything after it. Editing a tool definition clears the whole cache. Toggling web search, citations or fast mode clears the system and message cache. Prompts below the minimum, 512 tokens for Opus 5.5, are not cached at all.
In practice: keep tools fixed for the session, put anything that changes per call (dates, retrieved documents) after the cached part, and don’t reorder history.
It prices tokens only, at list prices: no batch, fast-mode, web search or data-residency pricing. On a Pro or Max plan, usage normally counts against plan limits, so read the dollars as an API equivalent. Models not in PRICES are skipped and named. Logs without the cache_creation split are priced as 5-minute writes. The what-if assumes the token counts stay the same when hits are lost. One session is one data point: run it on yours.
Reproduce it: cachecheck.py prints every number in the output blocks of sections 06 to 08; the per-token prices are in PRICES. The 5-minute and no-cache runs use copies of the log with the writes relabelled, or with all cached tokens moved into input_tokens. The $21, $200 and 165 steps come from the talk; the prices come from Anthropic's pricing page.
On a real Claude Opus 5.5 agent log, caching cut the list-price cost 9.2x, matching the talk’s warning from a different workload. What decides your cost is your hit rate, and a broken cache with 1-hour writes costs nearly twice as much as no cache at all. It takes one command to see which one you have.
What hit rate does your agent get, and did anything in this script surprise you?
If this saves you a bill, a clap helps others find it.
Follow me for daily LLM deep-dives.
Your Claude Agent Costs 9x More Without Prompt Caching was originally published in Towards AI on Medium, where people are continuing the conversation by highlighting and responding to this story.