Prompt Caching and Costs skill for Claude Code
Cut the LLM bill with caching before downgrades. Claude profiles where tokens actually go before changing anything, then applies the no-quality-loss levers in order: stable-prefix prompt caching with correct breakpoints, the batch API for offline work, and trimmed history and tool results. A bundled cost model shows break-even hit rates and tier what-ifs, and cheaper models or lower effort ship only after passing the eval.
- name
prompt-caching-costs - size189 lines · ≈2,200 tokens
- files2
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Cuts LLM API spend without losing quality by profiling token usage, then applying prompt caching with stable prefixes and cache_control breakpoints, batch processing, model and effort routing, context trimming, and output-length control, verified against an eval. Use when the LLM bill is too high, cache hit rates are low, cache_read_input_tokens stays at zero, when planning cost for a new AI feature, or when deciding whether to route traffic to a cheaper model. Try asking
- Our Claude API bill doubled last month. Find out why and cut it
- cache_read_input_tokens is always zero even though the system prompt never changes
- Estimate monthly cost for this feature and show what caching and batching would save
Use it when
- LLM API spend is high or growing faster than traffic
- Prompt cache hit rate is low or cache reads stay at zero
- You are budgeting a new LLM feature before launch
- You are considering routing some traffic to a cheaper model
Skip it when
- You need tracing and dashboards before you can see spend — set up llm-observability first
- The cost problem is cloud infrastructure, not tokens — use cloud-cost-audit
How to check the result
A repeated request within the TTL shows nonzero cache_read_input_tokens, and a week of production usage shows lower cost per completed task with the eval score unchanged.
The common mistake it prevents
Putting a timestamp or request ID at the top of the system prompt, which changes the prefix on every call so nothing is ever read from cache.
Cost work is done when you have a measured token profile (where spend goes by feature,
input vs. output, cached vs. uncached), the free wins are applied (caching, batching,
trimming), any quality-trading change (cheaper model, lower effort) passed the eval, and
the savings show up in logged usage, not in a spreadsheet estimate.
Ground rules
- Measure before optimizing. Pull
usagefrom logs or the provider’s usage reports and rank features by spend. - Free wins first, trade-offs second: caching, batching, and trimming do not change output quality; model downgrades and effort cuts might.
- Judge cost per completed task, not per request. A cheaper call that needs retries or extra agent turns is not cheaper.
- Every experiment that calls the model costs money. Estimate it and get the user’s approval before large eval runs.
Workflow
- [ ] 1. Build the token profile
- [ ] 2. Fix prompt caching (structure, breakpoints, invalidators)
- [ ] 3. Move non-urgent work to the batch API
- [ ] 4. Trim input: history, retrieved context, tool results, tool definitions
- [ ] 5. Control output: max_tokens, verbosity, reasoning effort
- [ ] 6. Route by difficulty (only with eval evidence)
- [ ] 7. Verify savings in production usage
1. Token profile
For each feature, collect over a representative window: request count, mean and p95
input tokens, cache_read_input_tokens, cache_creation_input_tokens, output tokens,
model. Then model the options:
python3 scripts/cost_model.py --model claude-sonnet-5-5 --requests 200000 \
--prefix-tokens 6000 --variable-tokens 800 --output-tokens 400 --hit-rate 0.9 --batch-share 0.3
The script prints spend with and without caching and batching, the output share of spend,
the break-even cache hit rate for the chosen TTL, and a what-if across model tiers. Its
built-in prices are a snapshot; confirm them on the provider’s pricing page or pass
--input-price/--output-price/--cache-read-mult.
Read the profile:
| Finding | First lever |
|---|---|
| Large repeated prefix (system prompt, tools, documents), low cache reads | Caching (step 2) |
| Offline/background volume | Batch API (step 3) |
| Input grows with conversation length or agent turns | Trimming (step 4) |
| Output > 50% of spend | Output control and effort (step 5) |
| Simple, high-volume requests on a top-tier model | Routing (step 6) |
2. Prompt caching
Caching is a prefix match: the provider reuses computation for an identical leading
sequence of tokens. Render order on the Anthropic Messages API is tools -> system ->
messages. Any byte change in the prefix invalidates everything after it.
Structure every request as stable first, volatile last:
resp = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=1024,
tools=TOOLS, # fixed list, fixed order
system=[
{"type": "text", "text": STABLE_INSTRUCTIONS},
{"type": "text", "text": REFERENCE_DOCS,
"cache_control": {"type": "ephemeral"}}, # breakpoint: caches tools + system
],
messages=history + [{"role": "user", "content": user_msg}],
)
u = resp.usage
log.info("cache", read=u.cache_read_input_tokens, write=u.cache_creation_input_tokens, uncached=u.input_tokens)
Placement rules (Anthropic specifics; other providers cache prefixes automatically or with their own controls, but the same ordering principle applies):
- Up to 4
cache_controlbreakpoints per request. Typical: end of system/tools, and the last message of the conversation so each turn reads the previous turn’s prefix. - A top-level
cache_controlon the request auto-places one breakpoint on the last cacheable block; use it when you do not need fine-grained placement. - Prefixes below the model’s minimum length (from 512 tokens on the newest Claude models up to 4,096 on some older ones) silently do not cache.
- Default TTL is 5 minutes, refreshed on every read.
{"type": "ephemeral", "ttl": "1h"}costs more to write (2x vs. 1.25x base input) and only pays off when requests sharing the prefix arrive 5-60 minutes apart. - Cache reads are billed at a small fraction of base input price (about 0.1x, lower on some current models) and writes at a premium, so a prefix pays off after roughly two requests at the 5-minute TTL.
- Caches are scoped per model and per workspace/organization. Splitting traffic across models or workspaces splits the cache.
Silent invalidators to grep for:
| Invalidator | Fix |
|---|---|
| Timestamp, date, request ID, or user name in the system prompt | Move to the final user message |
| Non-deterministic JSON (unsorted keys) in the prefix | json.dumps(obj, sort_keys=True) |
| Tool list built from a set or filtered per request | Fixed, sorted tool list per session |
| Retrieved documents reordered between turns | Append new context; do not reshuffle earlier blocks |
| Editing earlier messages (summaries rewritten each turn) | Append-only history; summarize in larger, rarer steps |
| Switching model, reasoning effort, or tool_choice mid-session | Keep per-session settings fixed |
Verify: send the same request twice within the TTL. The second response must show
cache_read_input_tokens > 0. If it is zero, diff the two rendered request bodies byte
for byte.
3. Batch API
Anthropic’s Message Batches API processes requests asynchronously (most finish within an
hour, up to 24 hours) at about half the price, and stacks with caching. Use it for evals,
backfills, nightly summarization, classification of stored data, and embedding-adjacent
enrichment. Key results by custom_id; they return in any order.
batch = client.messages.batches.create(requests=[
{"custom_id": f"doc-{d.id}", "params": {"model": "claude-haiku-5-5", "max_tokens": 512,
"system": SYSTEM, "messages": [{"role": "user", "content": d.text}]}}
for d in docs
])
# poll client.messages.batches.retrieve(batch.id).processing_status until "ended",
# then iterate client.messages.batches.results(batch.id)
4. Trim input
- Chat history: keep the last N turns verbatim plus a summary of older turns; or use the provider’s server-side compaction/context-editing features for long agent sessions.
- RAG: send the top 5-8 reranked chunks, not 30.
- Agent tool results: cap result size; clear or summarize old tool results.
- Tool definitions count as input on every call. Remove unused tools; load domain subsets.
- Count tokens with the provider’s counting endpoint (
client.messages.count_tokens); tokenizers differ across vendors and model generations.
5. Control output
- Output tokens cost several times more than input. Set
max_tokensto the real need and ask for concise formats (“3 bullets”, “label only”). - Reasoning tokens bill as output. On current Claude models tune
output_config: {"effort": ...}per route:lowfor classification and chat, higher for coding and agentic work. Measure; do not guess. - For classification, return a label from a structured output, not an explanation.
6. Route by difficulty
- Route cheap/easy traffic to a smaller model (
claude-haiku-5-5) and hard traffic to a larger one, via a rule (feature, input length) or a cheap classifier. - First test the simpler alternative: the stronger model at lower effort may match a cascade with one cache namespace and less code.
- Every route needs its own eval pass on real traffic samples (see
llm-evals).
7. Verify in production
Compare a week of usage before and after: spend per feature, cache hit ratio
(cache_read / (cache_read + cache_creation + input) on cacheable traffic), output tokens
per request, and cost per completed task. Confirm the eval score held.
Gotchas
- A 1-hour TTL on continuous traffic just doubles the write cost; reads already keep the 5-minute entry warm.
- Caching a prefix that changes every request (hit rate below break-even) costs more than not caching. The script’s break-even line flags this.
- Generation time counts against the TTL; a 4-minute response leaves 1 minute for the next request to hit a 5-minute cache.
- Parallel first requests all miss: warm the cache with one request before fanning out.
- Downgrading a model to save money without an eval is a quality regression waiting for a customer to report it.
Output contract
**Profile:** <period> spend <$>; top features <name: $ and %>; output share <x%>
**Applied (free):** caching (hit ratio <a> -> <b>), batch share <c>, trimming <what>
**Applied (trade-offs):** <model/effort changes> — eval <score before -> after>
**Measured savings:** <$ per period, %> from production usage, cost per task <x -> y> Bundled files
Loaded only when a step in the skill needs them. Review them here before installing.
scripts/cost_model.py
#!/usr/bin/env python3
"""Model LLM API spend with and without prompt caching, batching, and model routing.
Usage examples:
python3 cost_model.py --model claude-sonnet-5-5 --requests 200000 \
--prefix-tokens 6000 --variable-tokens 800 --output-tokens 400 --hit-rate 0.9
python3 cost_model.py --model claude-opus-5-5 --requests 5000 --prefix-tokens 20000 \
--variable-tokens 1000 --output-tokens 1500 --hit-rate 0.7 --ttl 1h --batch-share 0.5
python3 cost_model.py --input-price 3 --output-price 15 --cache-read-mult 0.1 ... # any provider
All prices are USD per million tokens. Built-in prices are a snapshot (late 2026) and
MUST be checked against the provider's current pricing page; override with
--input-price/--output-price/--cache-read-mult for anything else.
Exit code 0 on success, 2 on bad arguments.
"""
import argparse
import sys
# model -> (input $/MTok, output $/MTok, cache-read multiplier of input price)
PRICES = {
"claude-fable-5-1": (10.00, 50.00, 0.025),
"claude-opus-5-5": (4.00, 20.00, 0.05),
"claude-sonnet-5-5": (2.00, 10.00, 0.10),
"claude-haiku-5-5": (0.10, 0.50, 0.10), # prompts <= 100K tokens; higher tier above
}
WRITE_MULT = {"5m": 1.25, "1h": 2.0}
BATCH_DISCOUNT = 0.5
def money(x):
return f"${x:,.2f}"
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--model", default="claude-sonnet-5-5")
ap.add_argument("--requests", type=float, required=True, help="requests per period (e.g. per month)")
ap.add_argument("--prefix-tokens", type=float, default=0, help="stable, cacheable prefix tokens per request")
ap.add_argument("--variable-tokens", type=float, required=True, help="uncacheable input tokens per request")
ap.add_argument("--output-tokens", type=float, required=True, help="output tokens per request (incl. thinking)")
ap.add_argument("--hit-rate", type=float, default=0.0, help="fraction of requests whose prefix is a cache read")
ap.add_argument("--ttl", choices=["5m", "1h"], default="5m")
ap.add_argument("--batch-share", type=float, default=0.0, help="fraction of requests sent via batch API")
ap.add_argument("--input-price", type=float)
ap.add_argument("--output-price", type=float)
ap.add_argument("--cache-read-mult", type=float)
args = ap.parse_args()
if not 0 <= args.hit_rate <= 1 or not 0 <= args.batch_share <= 1:
print("error: --hit-rate and --batch-share must be between 0 and 1", file=sys.stderr)
sys.exit(2)
base = PRICES.get(args.model)
if base is None and (args.input_price is None or args.output_price is None):
print(f"error: unknown model {args.model}; pass --input-price and --output-price", file=sys.stderr)
sys.exit(2)
pin = args.input_price if args.input_price is not None else base[0]
pout = args.output_price if args.output_price is not None else base[1]
rmult = args.cache_read_mult if args.cache_read_mult is not None else (base[2] if base else 0.1)
wmult = WRITE_MULT[args.ttl]
n, P, V, O, h = args.requests, args.prefix_tokens, args.variable_tokens, args.output_tokens, args.hit_rate
per_m = 1_000_000
no_cache_in = n * (P + V) * pin / per_m
out_cost = n * O * pout / per_m
baseline = no_cache_in + out_cost
cached_in = (n * h * P * pin * rmult + n * (1 - h) * P * pin * wmult + n * V * pin) / per_m
with_cache = cached_in + out_cost
batch_factor = 1 - args.batch_share * BATCH_DISCOUNT
with_cache_batch = with_cache * batch_factor
print(f"model: {args.model} input ${pin}/MTok output ${pout}/MTok cache read x{rmult} write x{wmult} ({args.ttl})")
print(f"requests: {n:,.0f} prefix {P:,.0f} variable {V:,.0f} output {O:,.0f} tokens hit rate {h:.0%}")
print()
print(f"{'scenario':<34}{'input':>14}{'output':>14}{'total':>14}")
print(f"{'no caching':<34}{money(no_cache_in):>14}{money(out_cost):>14}{money(baseline):>14}")
print(f"{'with caching':<34}{money(cached_in):>14}{money(out_cost):>14}{money(with_cache):>14}")
if args.batch_share:
print(f"{'with caching + ' + format(args.batch_share, '.0%') + ' batched':<34}"
f"{'':>14}{'':>14}{money(with_cache_batch):>14}")
final = with_cache_batch
print()
saved = baseline - final
print(f"savings vs. no caching: {money(saved)} ({saved / baseline:.1%})" if baseline else "savings: n/a")
share_out = out_cost / baseline if baseline else 0
print(f"output share of baseline spend: {share_out:.0%}"
+ (" -> output dominates: trim verbosity, lower effort, or route to a cheaper model" if share_out > 0.5 else ""))
# Break-even hit rate: caching beats no caching when h*r + (1-h)*w < 1 -> h > (w-1)/(w-r)
be = (wmult - 1) / (wmult - rmult)
print(f"break-even hit rate for the prefix at {args.ttl} TTL: {be:.0%}"
+ (" WARNING: current hit rate is below break-even; caching costs more" if P and h < be else ""))
print("\nrouting what-if (same token profile, caching and batching applied):")
for m, (i, o, r) in PRICES.items():
ci = (n * h * P * i * r + n * (1 - h) * P * i * wmult + n * V * i) / per_m
total = (ci + n * O * o / per_m) * batch_factor
print(f" {m:<20} {money(total):>14}")
print("Routing only counts if the cheaper model passes your eval on this traffic.")
if __name__ == "__main__":
main()
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.