DEV Community

"Your cache hit rate is low" — true, and worth $0.16

Li Zhuojun on August 16, 2026

Anthropic emailed our org last week: prompt cache hit rate is low, and caching repeated content "could save up to 64% of direct API spend." The ema...
Collapse
 
reidmarlow profile image
Reid Marlow

The keep-alive ping section is the useful bit. Cache advice often stops at the hit rate, but the gap distribution decides whether the trick has any economic shape. I'd probably graph cost by idle gap next to the token table, because that's where the 1h TTL quietly changes the decision.

Collapse
 
lizhuojunx86 profile image
Li Zhuojun

Update on this. I solved the cap instead of picking it, and the answer stopped being a number.

Sweeping 1h to 12h in 15-minute steps: the best single cap is 10h, but it leads 9h45m by $7.63, and measuring one thing I had previously only assumed moved that same cap by $18.76. When a correction is larger than the distance between first and second place, the argmax is not the output. The band is 9 to 12 hours at a 55-minute cadence, net $663.69 to $811.30 over the window. The two ends are two runs, not a confidence interval: the low one treats all 54 gaps with no recorded model as cache-invalidating, the high one deducts only the 18 I can prove.

Those 18 are your question one level down. A keep-alive only holds the cache for the model it was written on, so if the session comes back on a different model the pings bought nothing. The rate climbs with idle length: 0% in the shortest decile of gaps, 23.1% past 24 hours. So leaving it unmeasured was not adding noise, it was pushing the cap long, and every version of that section before this one was answering with the bias in it.

One detail you might find as odd as I did: all 18 switches were opus-4-8 and fable-5, both directions. Not one switch down to a cheaper model. I had assumed mid-session model switching was mostly cost-driven. On this traffic it is not.

The window is pinned now, 2026-05-30 to 2026-08-16 behind a --benchmark flag, so the numbers reproduce. That flag and the sweep are on main rather than in 1.2.0, next release. The post is updated.

Collapse
 
lizhuojunx86 profile image
Li Zhuojun • Edited

Correction to my last comment: the window does not make it reproduce. Same window, same store, 432 expired gaps became 439 and 168 sessions became 174 inside a day — ingest picks up transcript files late, so closing the dates closes the timestamps and not the corpus. Net band is $650.53 to $806.82 now, and the shape of the answer is unchanged.

What I cite instead is a committed file, packages/traceguard/benchmark/data/001-traceguard-self-b07cc061.json, fingerprinted over the traces behind it. The tool refuses to overwrite an existing entry, so a re-run over grown traffic becomes a second file rather than a quiet edit to the first. Those two side by side are their own measurement: how far this drifts when nobody touches anything.

Collapse
 
lizhuojunx86 profile image
Li Zhuojun

Ran it, and the split changes the article's answer. Thanks for pushing on that.

Cost by bucket, same 71 days. 1–4h gaps: 183 of them, pings would have cost $81 against at most $887 of rewrite avoided. >4h gaps: 239 of them, pings cost $1,928 against at most $1,026.

So the aggregate verdict I published ($2,010 vs $1,913, NOT WORTH IT) is a bucket where pinging wins about 11:1 summed with a bucket where it loses about 2:1. The average hid both.

A policy you could actually run prospectively (ping every 55 minutes, give up after 4 hours idle) costs $316 including the pings wasted on the 239 gaps that outlive the cap, against at most $887 avoided. The honest claim is narrower than the one I made: unbounded pinging loses, capped pinging probably pays. Probably, because both sides of that comparison lean pro-ping — $887 is an upper bound, since cache_creation mixes the re-established prefix with whatever the turn genuinely added, and the $316 charges pings as pure 0.1x reads of a frozen prompt. That tilt is why the original refusal was safe and why this flip is only 2.8x of headroom, not 11x.

Your column is in the repo and ships in the next release: section 2 now carries rewrite and ping cost per bucket, and section 3 runs the capped policy next to the unbounded one. A verdict averaged over buckets that behave differently is not a decision, which is what the article was about, one level down. I've put an update note on the post too. Thanks for the nudge.

Collapse
 
articlefeed profile image
Boris Dzhingarov

64% of $0.25 is a brutal punchline. I'm from the SEO side and our tools pull the same trick, site health drops 8 points and the email never says what that means in actual traffic or money. Also had no idea Claude Code was already routing most writes to the 1h TTL. I had the keep-alive ping saved in my notes from some Twitter thread and apparently never needed it. Going to run cache_audit on my own logs, curious what a mostly vibe coded workload looks like.

Collapse
 
lizhuojunx86 profile image
Li Zhuojun

Quick update, since I handed you a cutoff number in that last reply.

The 4-hour cutoff was picked to match a table boundary, not solved for. Solving it gives a band instead: ping every 55 minutes, give up somewhere between 9 and 12 hours idle. Net $663.69 to $811.30 over a pinned window, and those two ends are two different runs rather than a confidence interval.

The SEO version of what I got wrong: I reported the peak of a curve without checking how flat the top was. Same shape as reporting a rank without the traffic behind it.

The two commands I gave you are unchanged and still work on 1.2.0, which prints the aggregate verdict only. The cap sweep and the pinned reporting window are on main and land in the next release. Once they ship, --benchmark is what makes your numbers and mine comparable rather than just similar.

Collapse
 
lizhuojunx86 profile image
Li Zhuojun

One correction to what I told you: I said --benchmark would make your numbers and mine comparable. It narrows the gap, it does not close it. The same window over my own store drifted inside a day, because late-arriving transcripts still land inside a closed date range.

Comparable now means matching the corpus fingerprint in the committed file, not just the dates. The SEO version: same date range is not the same crawl.

Collapse
 
articlefeed profile image
Boris Dzhingarov

Your SEO translation is more literal than you might know. Search Console does exactly what your late transcripts do: the last two or three days of any date range are provisional and quietly restate themselves after you've exported. Every SEO who builds weekly reports learns that the expensive way, usually when a client asks why last week's numbers changed since the last email. Our fix ended up the same shape as your fingerprint: freeze the export only after the data stops moving, and record when you pulled it, not just what you pulled.

Holding off on running mine until --benchmark ships, since an aggregate verdict I can't compare against yours is just a number that makes me feel something. Will send the table when the release lands.

Thread Thread
 
lizhuojunx86 profile image
Li Zhuojun

1.3.0 is out, so --benchmark and the per-bucket table are a pip install -U traceguard away. The share export is not in it. That landed on main after the release and goes out in the next one. Use your own dates rather than mine, though. Two orgs can never share a window, and that was never the thing that made numbers comparable. What the fingerprint buys is that one file names exactly one corpus, so nobody lines up two different pulls and calls it a trend.

Recording when you pulled it, not just what you pulled, is going into the schema. I had the what and not the when, which means a reader can see two of my entries disagree without being able to see why. Search Console's provisional tail is the same failure with a name on it: my window closes on Aug 16 and the corpus behind it is still filling in, so an export pulled the day it closed is a different object from one pulled a month later even when the dates match. The file will carry the pull time and the gap between it and the end of the window. You are credited in the benchmark README for it.

Client calls rather than build times is the run I most want to see. Every entry so far is one laptop doing one kind of work, and the gap distribution is the only part of this that is about the person rather than the tool. If a cache audit turns out to describe a marketer's calendar, that is your post and I would rather read it than write it.

Collapse
 
lizhuojunx86 profile image
Li Zhuojun

The SEO parallel is exact. Site health down 8 points and 64% of $0.25 are the same email: a rate with the denominator left out.

Two commands if you want your own numbers:

pip install -U traceguard
python -m traceguard.routing_audit.ingest --write --db sqlite:///traces.db
python -m traceguard.routing_audit.cache_audit --db sqlite:///traces.db
Enter fullscreen mode Exit fullscreen mode

First one reads ~/.claude/projects and is a dry run without --write. Second is read-only and prints the table. Checking those commands for you turned up a bug in my own README, which documented an ingest command that imported a module, ran nothing and exited 0. Fixed, along with the module that let it happen quietly.

On a vibe-coded workload I'd expect the hit rate to land close to ours, because Claude Code configures the caching and you don't. The section that will actually differ is the gap distribution, which is about how you work rather than what the tool does. Send it over if you run it.

One correction to my own post, since you had the ping trick in your notes: I split the counterfactual by gap length after Reid's comment, and pinging does pay on our 1–4h gaps ($81 to avoid up to $887). It only loses once you keep pinging across the very long idle stretches. So the note was not worthless, it just needs a cutoff.

Collapse
 
kartik-nvjk profile image
Kartik N V J K

The catch that the flagged 64% was structurally unreachable, every prompt unique and under the 1,024-token floor, is a great reminder that a true metric can still point at no money. I have been burned the same way by cost alerts that ignore prompt structure, so now I gate them on repeated-prefix ratio before they can fire. How are you computing list price for the Claude Code transcripts given they carry no cost field?

Collapse
 
lizhuojunx86 profile image
Li Zhuojun

There is no cost field to read, so it gets computed. I checked every local session file across CC 2.1.150 to 2.1.198 and there is no costUSD anywhere, so the audit prices message.usage against a hand-maintained table: input and output per MTok, cache reads at 0.1x, cache writes at 1.25x for the 5-minute TTL and 2x for the 1-hour one. Every entry in that table carries a source and the date it was verified against anthropic.com, sitting in the module docstring, because a price nobody can check is the same problem as a rate nobody can check.

Two things it does that a flat lookup would not. Prices are looked up by model and timestamp, because Sonnet 5 had an introductory price before a standard one and the same model id has to bill differently either side of that date. And a record with usage.speed == "fast" bills at the model's published fast multiplier, 2x on Opus 4.8 and Opus 5. Models with no published fast price return nothing rather than a guess.

That last part is where the n/a rows in the table come from. If a model has no published price I have verified, the audit counts its tokens and refuses to put a dollar figure on them. Two rows in the article print that way. It also carries the count of unpriced messages next to every money figure, 14 post-gap and 43 pre-gap in the current entry, so you can see how much of the bill is being left out rather than quietly folded in. A cost number resting on a guessed price is the same email I was complaining about.

Your repeated-prefix gate is better than anything I have on that side, and I do not have it. One thing worth wiring in if you build it out: the minimum cacheable prompt is model-dependent, 1,024 tokens on Opus 4.8 and 512 on Opus 5 and Fable 5, so a prefix ratio that ignores which model served the call will pass workloads that structurally cannot cache. That is the exact shape of the thing that produced my $0.16.