Where your Claude API bill comes from
An API bill is a simple sum you can reconstruct from the four counters on every response. The surprise line item is almost never the system prompt — it's cache writes and cache misses, driven by how you shape and time requests, not by how much text you send.
- Every response carries a `usage` object with four counters. Log them and you can rebuild every line of your invoice and see which kind of token is growing.
- Four token prices, like postage: cache read 0.1×, fresh input 1×, cache write 1.25× (5-min) / 2× (1-hour), output at its own higher rate.
- Put the stable stuff first and the changing stuff last, then set one `cache_control` breakpoint. A timestamp or user ID above the breakpoint silently turns caching off for the whole request.
- Trimming a cached system prompt saves ~10× less than the myth says. Cache misses and un-cached prefixes are where the money actually goes.
The subscription version of this cut me off for a week — I hit the Claude Max weekly cap twice running and blamed my instruction file, which turned out to be the wrong suspect. The pay-as-you-go API is the same machinery with a different meter: it doesn't cut you off, it just bills you. I flipped on extra-usage credits exactly once and watched it rack up $100 in twenty minutes — a small, memorable reminder that the meter never pauses to warn you. And when the invoice comes in bigger than last month, everyone reaches for the same wrong explanation.
The guess is almost always "our system prompt is too long." And the money almost always went somewhere else: a cache miss, invisible in a per-token chart, driven by how you shape requests rather than how much you cut. The good news is that unlike a subscription, an API bill is fully explainable — every response hands you the receipt.
The meter is dollars, and it's on every response
The API bills per token, and the price is a simple sum: each kind of token times its rate. Every response includes a usage object with four counters:1
input_tokens— fresh, uncached inputoutput_tokens— everything generated, including the model's "thinking"cache_creation_input_tokens— input written to cache this requestcache_read_input_tokens— input served from cache this request
Log those four per request and you can reconstruct every line of your invoice and — more usefully — see which kind of token is growing. Most teams only watch the total, and the total hides everything that matters. (The schema has since grown a nested cache_creation object that splits writes into ephemeral_5m_input_tokens and ephemeral_1h_input_tokens by cache tier; the four-counter mental model still holds, but read the sub-fields if you want the TTL breakdown.)1
Two asides worth banking. Cache reads don't count against your input-tokens-per-minute rate limit on current models, so caching raises your throughput ceiling, not just lowers your bill.1 And anything that isn't time-sensitive — evals, backfills, nightly summaries — belongs on the Batch API, which is 50% off.2
Four kinds of token, priced like postage
"Input tokens" is not one price. A model reads tokens (chunks of about 3–4 characters, roughly three-quarters of a word), and every input token is billed in one of three categories, weighted like postage — a letter and a parcel both go through the mail, they don't cost the same.3
Whenever a request is "50,000 tokens," ask which kind: 50,000 cache-read tokens cost about as much as 5,000 fresh ones.
The whiteboard: how caching works
The API has no memory between requests — each is a fresh envelope, so a chat re-sends its whole history every time. If every request re-processed its whole prefix (the stable beginning: system prompt, tool definitions, earlier turns) from scratch, big prompts and long conversations would be ruinous. They aren't, because of prompt caching.
The cache is a whiteboard in the model's office. The first time you send a block of context, the model writes it on the board — a cache write at 1.25× or 2×. Every later request that starts with the exact same prefix glances at the board — a cache read at 0.1×. On the API you control this: you place a cache_control marker (a breakpoint) on a content block, and everything up to that point becomes cacheable.3
- Request 1 · WRITE1.25–2× base
- Requests 2…N · READ0.1× each — cheap
- Gap or change · MISSboard erased
- Next request · REWRITEfull write price again
The rule that follows is the most important sentence here: put the stable stuff first and the changing stuff last. System prompt, then tools, then long reference material, then history, then the user's new message — breakpoint after the last stable block. Anything that varies before the breakpoint spoils the match for everything after it.
cache_creation_input_tokens is large next to cache_read_input_tokens, that's where the money is going — and no amount of prompt trimming fixes it.[^3]When the whiteboard gets erased
A cache miss rewrites the entire prefix at full price — not just what changed, all of it. Four common causes on the API:
1. The gap trap. The cache has a timer (a TTL). Every read resets it; let more time than the TTL pass between requests that share a prefix and the board is erased. For a chat product that's a user who wanders off; for a background job it's bursty traffic with quiet gaps, or a low-traffic customer whose shared prompt never stays warm. The timer is measured from the start of the request, so a slow four-minute response leaves only about a minute of a five-minute window.3
2. Something changed inside the prefix. The cache matches exact bytes. Change a word, list two tool definitions in a different order, or let a library serialize your tool list with its fields shuffled, and the prefix no longer matches. Even whitespace counts.3
3. Switching models. Caches are per model. Route the same conversation to a different model — deliberately, or because a routing layer picked one for you — and it starts with an empty board.
4. Dynamic content up front. The classic. You add the current date, the user's name, or a request ID to the top of the system prompt. Now every request has a different prefix, nothing caches, and every request is a full-price write. The same thing happens the moment you "optimize" a live system prompt: every active conversation misses at once.
There are two cache tiers — the default 5-minute (1.25× to write, goes cold fast) and the 1-hour (2× to write, survives gaps). The break-even is simple: the 1-hour tier costs an extra 0.75× on one write; if it saves even a single rewrite you'd have paid 1.25× for, it's already ahead. Bursty or low-frequency traffic wants the 1-hour tier; a tight loop of requests seconds apart is fine on five minutes.3
Where the dollars actually go
The ranking of what inflates an API bill looks nothing like the folk wisdom. Highest impact first — the top three amplify each other:4
A note on tool-result bloat: a tool returns a 40 KB JSON blob, you append it to the message list, and it now rides along in every future request of that conversation — every receipt in the backpack, and you carry the backpack everywhere. Summarize or truncate tool results before they enter history. And on heavy tool catalogs: one measurement put a five-server MCP setup at ~55,000 tokens of tool definitions before any user input.5 Claude Code now loads tool descriptions on demand once they exceed roughly 10% of the context window, but a raw API integration doesn't — it sends the whole catalog every time.6
The playbook
Same shape as the companion post: none of these send less to the model; they change the order, the timing, and what rides along.
- Stable first, dynamic last, then mark the line. Order the request as in the diagram above and set one
cache_controlbreakpoint after the last stable block:
"system": [
{ "type": "text", "text": "You are a support assistant for Acme. …(stable)…" },
{ "type": "text", "text": "…(product manual, stable)…",
"cache_control": { "type": "ephemeral" } }
],
"tools": [ …stable tool definitions… ],
"messages": [ …history…, { "role": "user", "content": "the new question" } ]
- Match the cache tier to your traffic. Requests sharing a prefix land more than five minutes apart? Switch that breakpoint to
"ttl": "1h". Seconds apart? The default is fine. - Send the exact same bytes. No dates or IDs in the prompt; sort your tool list and serialize it with stable key order; load the prompt once from a versioned file, not per request. A prefix that varies is just an uncached prefix you're paying write prices for.
- Trim what enters history. Cap tool results before appending them; ask the model for the fields you need, not the whole payload; summarize old turns.
- Route by task, and give thinking a budget. Extraction and classification don't need the top model or deep reasoning — and thinking is billed as output. Keep one conversation on one model (caches are per model).
- Log the four counters and alert on the ratio. This is the early-warning system for every mistake above:
u = response.usage
log({ "input": u.input_tokens, "output": u.output_tokens,
"cache_write": u.cache_creation_input_tokens,
"cache_read": u.cache_read_input_tokens })
Read the ratio like a diagnosis: cache_read ≫ cache_write means caching works, so look at context size and model; cache_write large with gaps in traffic points at the TTL; cache_write ≈ input every request means something varies in the prefix; all counters zero means no breakpoints are set.
The reframe
The lesson isn't "shorten the prompt." It's stop paying to rewrite the cache — and read the evidence before you act. The usage object is your audit log; the fix follows from what it says, and it never once says "trim the system prompt first." Cache misses are the expensive event, ordering is the cheapest fix, and the biggest cached document is nearly free.
Reading usage per request is the API version of a habit I wanted everywhere. On the Claude Code side I started building AgentWrangler — a local dashboard that reads the tool's own session logs, shows where the tokens went, and flags the oversized context and the model mismatch before the bill does. It keeps only the counts and discards the content, which is the audit-evidence posture I'd want for my own telemetry. It's Claude-Code-focused, and the code's still local; an early PRD is up at github.com/Doogit/AgentWrangler if you want to follow along. Stay tuned.
Footnotes
-
Anthropic, Prompt caching and API rate limits documentation — the four
usagecounters (with the newer nestedcache_creationsub-fieldsephemeral_5m_input_tokens/ephemeral_1h_input_tokens); cache-read tokens do not count toward input-tokens-per-minute limits on current models. platform.claude.com/docs/en/docs/build-with-claude/prompt-caching. First-party. ↩ ↩2 ↩3 -
Anthropic, models overview — "Batch API requests are 50% off"; extended thinking is billed as output tokens and counts toward output rate limits. platform.claude.com/docs/en/docs/about-claude/models/overview. First-party. ↩
-
Anthropic, Prompt caching documentation — cache-write 1.25× (5-minute) / 2× (1-hour) and cache-read 0.1× of base input; exact-prefix matching, any change (including whitespace) invalidates; the lifetime is measured from the start of the request. First-party. ↩ ↩2 ↩3 ↩4 ↩5
-
Build to Launch, "Claude Code Token Optimization" (2026), and Anthropic documentation — ordering of cost drivers adapted for API workloads from practitioner reports and docs, not a controlled measurement. Community/practitioner + first-party. ↩
-
startdebugging.net (May 2026) — a typical five-server MCP setup measured at roughly 55,000 tokens of tool definitions loaded before any user input. Community/practitioner. ↩
-
Anthropic, MCP tool search (Jan 14, 2026, Claude Code v2.1.7) — defers MCP tool descriptions when they exceed roughly 10% of the context window, loading only the relevant handful per query. First-party. ↩