maborak.com~/blog/prompt-cache-misses-cost-leaksoftware & infrastructure engineering
Back to all articles

Prompt cache misses are a silent cost leak in coding agents

A background agent that fires after the cache expires pays full price to rebuild it. How to read the cache numbers and stop the leak.

By the Maborak engineering team · · 8 min read

A reported incident: subagents were filing watchers that never exited, and the coordinator renewed them after the prompt cache had expired. Each renewal re-processed a large context and wrote it back to the cache at full input rates. In under 48 hours it consumed roughly $4k of tokens, noticed only afterwards. [reported] — Jared Starkweather.

Checked 29 October 2026. Caching mechanics and pricing multipliers are from Anthropic’s prompt-caching and Claude Code cost documentation. The measurement at the end is from this machine. The incident figure is a third-party report, not a measurement of ours.

Background

Anthropic prompt caching lets a request resume from a cached prefix. A cache write costs 1.25× the base input token price for a 5-minute entry, and 2× for a 1-hour entry; a cache read costs 0.1× the base input price. The default entry lives 5 minutes, refreshed at no extra cost each time it is read. The cache is keyed on the full prefix up to the breakpoint — tools, then system, then messages — so any change above the breakpoint writes a new entry. Anthropic prompt caching.

Claude Code applies this automatically: it caches the conversation prefix and reports the result. From the Claude Code cost guide, the /usage Session block adds a Prompt cache (main) line, for example:

Prompt cache (main): 14 requests · 91% of input tokens from cache · 2 misses (last 6m 10s ago, 310.2k tokens re-cached) · warm (1h TTL, last activity 40s ago)

That line is the whole argument for watching this number: it reports cache reads as a percentage, the misses, how many tokens those misses re-wrote, and whether the cache is currently warm. Claude Code: manage costs.

Mechanics

A cached prefix only helps while it is warm. Three properties combine badly with an idle or recurring agent:

  • The TTL is short. After five minutes without a read (one hour if configured), the entry is gone. The next request is a miss: it processes the prefix again and writes a fresh entry.
  • Writes are priced above fresh input. A miss on a large prefix costs more than sending it uncached, because the write is 1.25× the input price (2× for 1h). A miss is not neutral; it is a penalty.
  • The prefix is the whole context. A long session carries the conversation, tool results and instructions. A miss re-processes all of it.

So the dangerous shape is not a heavy interactive session. It is a small amount of work on a large context, fired on a schedule that outlives the cache. A background watcher that wakes, sends a 100k-token context, writes it to the cache, and sleeps past the TTL does that again on every wake. A coordinator that keeps such watchers alive turns a one-off into a steady stream. The Claude Code cost guide lists the same sources explicitly: session-scoped scheduled tasks that fire while a session is idle, cross-session messages delivered into an idle session, and goal check-ins — each of which sends the full context as a new turn.

Subagents make it worse in a way that is easy to miss, because they are not in the main cache line. Every subagent sends its own requests with its own context on top of the main conversation’s. The Claude Code guide puts agent teams at roughly 7× a standard session, and notes that a teammate keeps consuming tokens until it exits.

Impact

The leak is invisible in normal use: it is background work, it produces plausible output, and a per-session dollar figure can look normal while the aggregate climbs. On a subscription it burns the seat allowance; on API billing it is money. The signal that separates a healthy fleet from a leaking one is not total tokens but the ratio of cache reads to cache writes, and the rate of large writes.

Probe

Claude Code stores a transcript per session under ~/.claude/projects/<project>/<session>.jsonl. Each assistant entry carries the API usage fields. Summing them per machine gives the hit rate and exposes repeated large writes.

import json, glob, os
from collections import defaultdict

root = os.path.expanduser('~/.claude/projects')
seen, reqs = set(), 0
tot = {'input': 0, 'cc': 0, 'cr': 0, 'out': 0}
sess = defaultdict(lambda: {'cc': 0, 'cr': 0, 'n': 0})
big = 0  # requests that wrote >=50k to cache: a miss on a large context

for f in glob.glob(root + '/**/*.jsonl', recursive=True):
    for line in open(f, errors='replace'):
        try:
            e = json.loads(line)
        except Exception:
            continue
        m, u = e.get('message'), None
        if isinstance(m, dict):
            u = m.get('usage')
        if not isinstance(u, dict):
            continue
        mid = m.get('id')
        if mid and mid in seen:
            continue          # streaming can repeat a request; dedupe by id
        if mid:
            seen.add(mid)
        it = u.get('input_tokens', 0) or 0
        cc = u.get('cache_creation_input_tokens', 0) or 0
        cr = u.get('cache_read_input_tokens', 0) or 0
        tot['input'] += it; tot['cc'] += cc; tot['cr'] += cr
        tot['out'] += u.get('output_tokens', 0) or 0
        reqs += 1
        s = sess[e.get('sessionId', '?')]; s['cc'] += cc; s['cr'] += cr; s['n'] += 1
        if cc >= 50000:
            big += 1

total_in = tot['input'] + tot['cc'] + tot['cr']
print(f"requests {reqs}, total input {total_in:,}")
print(f"  cache read  {tot['cr']:,} ({tot['cr']/total_in*100:.1f}%)")
print(f"  cache write {tot['cc']:,} ({tot['cc']/total_in*100:.1f}%)")
print(f"requests writing >=50k to cache: {big}")
for sid, s in sorted(sess.items(), key=lambda x: -x[1]['cc'])[:5]:
    print(f"  {sid[:8]}  write {s['cc']:>12,}  read {s['cr']:>12,}  requests {s['n']}")

Run on this machine, over 1,165 transcripts: [reproduced]

files         1165
requests      75443
total input  27,193,261,438
  cache read  26,815,897,778 (98.6%)
  cache write 377,211,278 (1.4%)
  uncached    152,382 (0.0%)
output        20,344,391
requests writing >=50k to cache: 1234
top sessions by cache write:
  e9a7582c  write   80,859,546  read 8,123,767,557  requests 24758
  4d80925e  write   62,319,647  read 3,859,050,152  requests 11358
  ae66c2c6  write   55,589,927  read 3,972,046,379  requests 8270

The aggregate looks healthy: 98.6% of input tokens were read from cache. The leak is in the edges. 377M tokens were re-written to cache, and 1,234 requests wrote 50k tokens or more in a single request. The busiest single session wrote 80.8M tokens back to the cache across 24,758 requests. On a 5-minute TTL, 24,758 requests over a long day is consistent with frequent misses; each large write is a full-price rebuild of a large prefix. The probe does not attribute those writes to a scheduled task or a subagent; it shows where to look. For that attribution, Claude Code’s /usage breakdown lists recent scheduled tasks (“Loops”) ordered by total tokens, and the attribution section shows the subagent and MCP shares.

The probe reads local session files. Re-run it before and after a change; the cache write figure and the count of large writes are the numbers to move.

Mitigation

Order: stop the unbounded work, then bound it, then cap the blast radius, then watch.

  1. Bound background work. A watcher that “can never exit” is the defect. Give every scheduled task, poller and subagent a timeout and a maximum number of runs, and a cancellation path the coordinator honours. Prefer a task that finishes to one that idles.
  2. Do not keep a large context warm with small, periodic work. Either batch the work into a session, or accept the cache is cold and keep the context small. A scheduled task over a large context at a short interval pays the write every time.
  3. Choose the TTL deliberately. If periodic work has to run on a large context, a 1-hour entry (2× write, 0.1× read) can be cheaper than repeated 5-minute misses — but only if the interval actually fits inside it and the context is genuinely reused. Measure both.
  4. Cap the spend. For API users, --max-budget-usd stops a session at a dollar figure. For teams, workspace spend limits and the modelPricing setting make reported figures match contracted rates.
  5. Shrink the context. /clear between unrelated tasks; delegate verbose work to a subagent so its output does not enter the main context; run simple subagents on a smaller model; keep CLAUDE.md small and move workflow detail into on-demand skills. Each of these reduces the cost of the next miss, not only the steady state.
  6. Turn off the idle sources you do not need. Idle check-ins, cross-session delivery and session-scoped scheduled tasks each send the full context when the session is otherwise quiet. The cost guide documents the switches.

Verification

  • /usage in an interactive session: the Prompt cache (main) line shows the read percentage, the misses, the tokens re-written and whether the cache is warm. A rising misses count with large re-cached values is the leak.
  • Re-run the probe above. The targets are the count of requests writing ≥50k and the per-session write column, not the headline read percentage.
  • For a fleet, export Claude Code telemetry over OpenTelemetry and alert on cache-write volume and on the miss rate, rather than on total tokens alone.

A healthy fleet is not one with zero cache writes. It is one where large writes are rare, expected and attributable — a compaction, a deliberate context change, or a cold start — and not a background process quietly rebuilding the same prefix on a timer.

Sources

Back to all articles

Technologies we work with

  • Argo CD
  • Kubernetes
  • Docker
  • Podman
  • Ansible
  • Jenkins
  • Chef
  • Proxmox VE
  • Google Cloud
  • Linux
  • Red Hat
  • CentOS
  • NGINX
  • Apache
  • Traefik
  • Prometheus
  • Grafana
  • PostgreSQL
  • MySQL
  • MariaDB
  • Redis
  • etcd
  • Qdrant
  • Elasticsearch
  • Logstash
  • Kibana
  • PagerDuty
  • Go
  • Python
  • FastAPI
  • PHP
  • TypeScript
  • React