{"slug": "claude-code-costs-act-i-how-the-billing-actually-works", "title": "Claude Code Costs, Act I — How the billing actually works", "summary": "An engineer reverse-engineered Claude Code's billing model by pointing the client at a local proxy that logs each API request. The analysis reveals that the client can under-report cache-write costs by using a 1.25× rate instead of the published 2× rate, and that every turn is a complete HTTP request with no privileged server-side session. The guide provides four mental models for predicting and reducing costs, with all claims backed by provenance tags indicating measurement, documentation, or both.", "body_md": "*How billing actually works, the ecosystem of options to spend less, and the mistakes that quietly cost you money — grounded in measurement, not folklore.*\n\nThis guide teaches the cost model of [Claude Code](https://claude.com/claude-code) from first principles. It is written for engineers who want to **predict** their bill, **lower** it deliberately, and **recognize** the anti-patterns before they hit production. Every non-obvious claim carries a provenance tag — **[measured]** (observed on the wire), **[docs]** (from Anthropic's published documentation), or **[doc-confirmed]** (measured *and* matching the docs) — and the table or experiment that supports it.\n\nThe best way to use this guide:keep a Claude session open on the side as you read. When something isn't clear, paste it in and ask Claude to explain it or to dig deeper. Better still, don't take the numbers on faith — ask Claude toreproduce the experimentson your own setup, walk you through what the results mean, andshow you the raw request/response logsbehind each table. The provenance tags exist so you can verify everything here yourself; treat this guide as a starting point for that conversation, not the last word.\n\n`usage`\n\nblock and tell the difference between a warm session and one that is silently rebuilding its cache every turn.The guide is built around **four mental models**. Learn them and the rest is derivable:\n\nThe guide is in four acts mapping to what you're trying to do: **Act I** — how billing works; **Act II** — where the big hidden costs are (model switching and thinking blocks); **Act III** — the ecosystem of options for spending less; **Act IV** — the consolidated mistakes catalogue and a one-page cheat sheet.\n\nNone of this relies on internal access. Claude Code honors the `ANTHROPIC_BASE_URL`\n\nenvironment variable, so it can be pointed at a plain-HTTP local reverse proxy that logs each `/v1/messages`\n\nrequest body and forwards it to `https://api.anthropic.com`\n\n. There is no TLS interception: the OAuth `Authorization: Bearer`\n\ntoken and `anthropic-beta`\n\nheaders pass through untouched; the proxy rewrites only the JSON `model`\n\nfield when an experiment calls for it. The proxy parses the streamed (SSE) response for the `usage`\n\nblock and counts `cache_control`\n\nmarkers. A replay harness re-sends real captured requests verbatim — genuine headers — under single-variable variations, so each finding isolates one cause.\n\n**The honest constraint throughout:** the API exposes no field that announces \"thinking was dropped\" or \"your cache was busted.\" Every conclusion here is *inferred* from HTTP status codes, token-count deltas between near-identical requests, and inspection of the response. Where a claim is inferred rather than directly reported, it says so.\n\nMeasurements were taken against Claude Code 2.1.150 (OAuth auth, mid-2026) on Sonnet 4.6, Opus 4.7/4.8, and Haiku 4.5. Fable 5 and the Mythos family were gated (HTTP 404) on the test host, so claims about them are documentation-only. Prices and exact behaviors are **version-specific** — re-verify them for your own models and client version.\n\nBilling-rate caveat, stated once and used throughout.Claude Code's self-reported`total_cost_usd`\n\n(what`/cost`\n\nand the status line show) canunder-reportcache-write cost: it prices the 1-hour-TTL writes it sends at the 5-minute1.25×rate. Anthropic's published rate for a 1-hour-TTL write is2×base input — the rate this guide uses everywhere. So expect your displayed session cost to readlowerthan the figures here. Treat the Anthropic Console as authoritative for actual billing. This gap is itself one of the mistakes in Act IV.\n\nThe entire cost model falls out of one architectural fact. Get this fact and the three cost buckets, and you can already predict most of your bill.\n\nClaude Code is a *client* of Anthropic's `/v1/messages`\n\nHTTP API. It holds no privileged channel to the model and no special server-side session. Every turn is **one complete HTTP request** that carries the entire context the model will see: the tool definitions, the system prompt, and every message in the conversation so far. The server generates a response and then **forgets everything** — no session, no memory, no \"conversation\" object. The next turn re-sends the whole thing again, one turn longer.\n\nThat single fact — **the request is the only state** — drives every cost lever in this guide:\n\nPicture a brilliant contractor with no long-term memory. Every time you consult them, you must hand over a complete dossier: the tools they're allowed to use, their standing instructions, and the entire history of the project so far. They read it, give you excellent advice, and then forget the entire engagement the instant you leave. Next time, you bring the same dossier plus today's new page.\n\nThat is exactly the relationship between Claude Code and the API. Everything expensive about Claude Code follows from \"the dossier gets re-read, in full, every single time.\"\n\nTwo requests, same session design, isolating whether the server remembers a fact across turns:\n\n| request sent | model's answer |\n|---|---|\n| \"my secret is 42\" + acknowledgement + \"what's my secret?\" (the fact is in the payload) | \"42\" |\n| only \"what's my secret?\" (the prior turn omitted from the payload) | \"I don't have a secret number stored in memory…\" |\n\nThe model knows only what the *current request* carries. When the prior turn is omitted from the request body, the secret is gone — there is no session to recall it from. The \"memory\" you experience in a chat is an illusion maintained entirely by the client re-sending history.\n\nIt isn't just visible messages. A captured Opus continuation shows message `[17]`\n\nas `['thinking(sig=yes, len=0)', 'text', 'tool_use(Grep)']`\n\n, followed by `[18]`\n\nas a `tool_result`\n\n. The thinking block is *in the request body* — the client is sending the model's own prior reasoning back to it. (What those blocks contain, how they're billed, and what happens to them on a model switch is the whole of Act II's second half. For now: they are part of the dossier, and parts of the dossier cost money.)\n\nA subtlety worth internalizing early: **resending thinking is not strictly mandatory in this configuration.** Replaying the captured continuation with thinking blocks (a) kept, (b) stripped from the last assistant turn, and (c) stripped everywhere all returned HTTP 200 **[measured]**. The server doesn't error and doesn't substitute a remembered copy — because it has none. Resending thinking is how the *client* gives the model reasoning continuity; it is not how the server maintains state. The server maintains no state.\n\nEvery request is assembled in this order:\n\n```\ntools  →  system  →  messages\n```\n\nTool definitions sit at byte 0, the system prompt next, and the conversation last. This isn't cosmetic. It is the single most important *layout* decision for cost, and it follows one rule:\n\nStable content first, volatile content last.\n\nYou'll see why this ordering is load-bearing the moment we get to caching: the cache is a *prefix* match, so the things that change least must come first, or they'll keep getting invalidated by the things that change most.\n\nOne more piece of anatomy, because it decides cache behavior later. The `messages`\n\ntier is an ordered list of **messages** — conversation turns, each with a `role`\n\nand some content — and each message's content is itself an ordered list of typed **content blocks**: a text block, a `tool_use`\n\nblock (the model calling a tool), a `tool_result`\n\nblock (your answer to it), a thinking block, an image. **One message can hold many blocks** — an assistant turn that fires ten parallel tools is a *single* message but twenty-plus blocks. Hold onto the message-vs-block distinction: the cache's backward re-link counts *blocks*, not messages, which is exactly what a big tool burst trips (see *Agentic tool bursts overflow the 20-block lookback*).\n\nEvery request's `usage`\n\nblock breaks input into three buckets, plus output. Learn what each one *costs relative to base input*, because that ratio is the whole game:\n\nBucket (`usage.*` ) |\nMeaning | Price vs. base input |\n|---|---|---|\n`cache_read_input_tokens` |\nserved from an existing cache entry | 0.1× |\n`cache_creation_input_tokens` |\nwritten to the cache this request |\n2× (1-hour TTL) |\n`input_tokens` |\nprocessed uncached, not cached | 1× |\n`output_tokens` |\ngenerated tokens | the model's output rate |\n\nTwo facts to burn in:\n\nRe-verify at `claude.com/pricing`\n\n. Cache read = 0.1× input; 1-hour cache write = 2× input; output = 5× input.\n\n| Model | Input (1×) | Cache read (0.1×) | Cache write, 1h (2×) | Output (5×) |\n|---|---|---|---|---|\n| Fable 5 | $10.00 | $1.00 | $20.00 | $50.00 |\n| Opus 4.8 | $5.00 | $0.50 | $10.00 | $25.00 |\n| Sonnet 4.6 | $3.00 | $0.30 | $6.00 | $15.00 |\n| Haiku 4.5 | $1.00 | $0.10 | $2.00 | $5.00 |\n\nThe ratios are clean and worth memorizing: **Sonnet = 0.6× Opus, Haiku = 0.2× Opus, Sonnet = 3× Haiku.** They'll matter when we compare routing options.\n\nCaching is a trade: you pay a one-time **expensive write** (2× the input price) so that later requests get **cheap reads** (0.1×). Whether that trade wins depends on how many times you'll re-read the cached prefix.\n\nProcess the same prefix over `N`\n\nrequests, two ways:\n\n`N × 1×`\n\n`2× + (N−1) × 0.1×`\n\nCaching comes out ahead once `2 + 0.1(N−1) < N`\n\n— i.e. **from the 3rd request onward.** Worked out:\n\n`2 + 0.1 + 0.1 = 2.2×`\n\nvs `3×`\n\nuncached. ✅ cache wins.`2.9×`\n\nvs `10×`\n\n. ✅ cache wins comfortably.**Why \"3rd request\" and not \"2nd\"?** The TTL sets the write price, and this is exactly where Claude Code differs from the raw API. By default Anthropic uses a **5-minute** cache TTL, where a write costs only **1.25×** — so caching breaks even one request sooner, on the **2nd**. But the Claude Code client **overrides that default and requests the 1-hour TTL** on every breakpoint (measured later in this act), where a write costs **2×** — pushing break-even to the **3rd** request. This guide uses 2× / 1-hour throughout because that's what Claude Code actually sends.\n\n⚠️ Mistake — paying for a cache you never read back.Awasted write(content cached but never read again) costs2×—doublewhat you'd have paid had you never cached it. Caching is not free insurance; it's a bet that you'll re-read the prefix at least three times. A short, one-shot interaction that ends after one or two turns can becheaperwithout caching.\n\n✅ Fix— Let Claude Code's defaults stand for interactive sessions (they will be re-read many times). Only worry about this in custom harnesses that cache aggressively but terminate early.\n\nOne more corollary you'll lean on constantly: **every read also refreshes the TTL.** A continuously-reused prefix never expires.\n\n**What to do (Act I so far):** Internalize that cost = (re-processing history) + (output you generate). The cache is the only tool against the first term, and it's a strict prefix match — which is the next mental model.\n\nThis is the model that, once you have it, makes cache behavior obvious instead of mysterious — and it starts one level down, in how the model reads your prompt at all.\n\nThe model itself — a **transformer** (the neural-network architecture every modern LLM, Claude included, is built on) — reads your prompt token by token, left to right. A **token** is roughly a word or a word-piece. For every token the model computes three vectors — a **query**, a **key**, and a **value** — and each token *attends* to all the tokens before it: its result is a blend of those earlier tokens' values, weighted by how well its query matches their keys.\n\nIn plain terms:to handle each word, the model glances back over everything before it and weighs which earlier words matter — the way you look back to figure out what \"it\" refers to in\"I poured water into the glass until it was full.\"It does that for every token, against every earlier token at once.\n\nTwo facts fall straight out of this, and together they explain the entire cache:\n\nThe prompt cache stores exactly those computed **key/value vectors — the \"KV cache\" — for the prefix tokens**, keyed to the exact tokens that produced them. On a hit, the server **loads that saved attention state instead of recomputing it** and starts real work only at the first uncached token. Every pricing rule in this guide follows from that one move:\n\n`output_tokens`\n\n, absent from `cache_creation`\n\n. The output `cache_creation`\n\n), then read warm thereafter. So a generated span is paid once as output, once more as a single cache write next turn, then cheap reads — there's no cached output-KV to reuse, only the re-encoded text. (Measured: a 440-token turn-1 output had `cache_creation`\n\n0 that turn, then appeared as ~450 of turn-2's `cache_creation`\n\n, then rode along in turn-3's warm read. That's why output is its own, uncacheable-at-generation cost line back in\n\nPrompt caching is a strict prefix match.A cache entry is keyed on the exact tokens from position 0 up to a cut point.Change one token at position N and every cached state at position ≥ N is invalid— because each of those later key/value vectors was computedattending tothe token you changed.\n\nThe cache is therefore **content-addressed**, not position-addressed: the API re-hashes the leading tokens each request and looks the hash up. Identical leading bytes → hit. This is why the render order (`tools → system → messages`\n\n) is load-bearing — put the bytes that never change at the front, and the model **reloads** the prefix's attention state instead of recomputing it.\n\nThe cut point itself is a **cache breakpoint** — a `cache_control`\n\nmarker on a block, meaning \"cache everything from the start up to here.\" In Claude Code the client places these for you; the rest of this section is what they do.\n\nHere's the elegant part. The *last* breakpoint slides forward to the newest turn on each request (Claude Code does this automatically; on the raw API you do it yourself). Then three things happen together:\n\nIn one line:\n\n```\nwrite cost per turn ≈ (new tokens since last boundary) × 2× rate\n```\n\nThe exception is the whole story of cache-busting: **a byte change inside the prefix** misses at that point, and the next write must span from the change forward — now 2× as costly to rebuild as the read it replaced.\n\nThe slide also has a **reach limit**: a breakpoint only re-links to a cache entry within **~20 content blocks** of it. So a single turn that appends more than ~20 blocks — a large agentic tool burst — breaks the chain and forces a cold rewrite even when nothing else changed. That failure mode, and the proxy-side fix, are in *Agentic tool bursts overflow the 20-block lookback* (under *What busts the cache*).\n\nMemorize which changes survive (✅) and which invalidate (❌) each tier:\n\n| Change | Tools | System | Messages |\n|---|---|---|---|\n| Tool definitions (add/remove/reorder) | ❌ | ❌ | ❌ |\n| Model switch | ❌ | ❌ | ❌ |\n`speed` / web-search / citations toggle |\n✅ | ❌ | ❌ |\n| System prompt content | ✅ | ❌ | ❌ |\n`tool_choice` / images / thinking toggle |\n✅ | ✅ | ❌ |\n| Message content | ✅ | ✅ | ❌ |\n\nSource: [Anthropic — Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) (cache-tiers / what-invalidates section).\n\nThe two rows that force a **full rebuild** — touching every tier — are the expensive ones: **tool-definition changes** and **model switches.** Because both sit at or before byte 0 of what's cached, they re-key everything, and at the 2× write rate that rebuild is twice as painful as a read.\n\nClaude Code places the cache breakpoints for you — three of them, 1-hour TTL (proven in *Claude Code's actual caching*) — so you never write `cache_control`\n\nyourself. Your cache lever isn't *placing* breakpoints; it's **not disturbing the prefix the client already cached.** The ways a real session loses it:\n\n`@import`\n\n`--append-system-prompt`\n\n, output styles, and the like sit in the `<system-reminder>`\n\nin `messages[0]`\n\n(the first user turn), placed `messages[0]`\n\nsnapshot, so the warm prefix is safe no matter who edits the file. Headless `-p`\n\nignores the edit until restart. Interactive surfaces it cheaply `messages[0]`\n\nstays byte-identical.`--continue`\n\nor a fresh start → the bill lands, but only if the file changed.`messages[0]`\n\nby re-reading CLAUDE.md (and its `@import`\n\ns) from disk. `cache_read`\n\ncollapses to the system tier and the `cache_read`\n\n30,216→27,975, `cache_creation`\n\n24→2,265 vs an unedited control; an `@import`\n\ned file re-keys identically (45→2,301). `messages[0]`\n\ncomes back byte-identical and only the sliding tail re-keys (~17–21 tokens). So the re-key is the `system[2]`\n\n's hash (a CLAUDE.md re-key leaves `system[2]`\n\nidentical and flips only `messages[0]`\n\n). `messages[0]`\n\n(same tier as CLAUDE.md, after the system breakpoints); a plugin/MCP server's `--continue`\n\n-invisible.`-p`\n\nignores it. It's baked into the canonical `messages[0]`\n\nonly at the next `--continue`\n\ndoes `--continue`\n\nre-reads CLAUDE.md but `cache_read`\n\n29,424→0 and cold-wrote all 29,505 tokens). A connected server's own tool change (e.g. a dynamic `tools/list_changed`\n\n) applies The full catalogue of what busts the prefix — silent invalidators, dynamic MCP tools, injected date/git/system-reminders — is the next section.\n\nNote — extra gotchas if you use the Claude API directly.Everything above assumes the Claude Code client, which manages caching for you. If you assemble`/v1/messages`\n\nrequests yourself, you also own the things Claude Code quietly handles:\n\nYou must addWith no breakpoint,`cache_control`\n\nyourself.nothingis cached. There's amaximum of 4per request; place them at stability boundaries —`[tools + system]`\n\n(frozen) /`[project context / CLAUDE.md]`\n\n(per task) /`[conversation]`\n\n(the sliding tail) — and the API reads the longest matching prefix, reprocessing only what follows.Minimum cacheable prefix ~1,024–4,096 tokens(model-dependent). Below it the marker silently no-ops:`cache_creation_input_tokens`\n\ncomes back`0`\n\nand nothing is cached, even though it looks enabled.The default TTL is 5 minutes(write ≈ 1.25× input); you must explicitly request the1-hourTTL (write ≈ 2×) that Claude Code always sends. Choose by how long you'll keep reusing the prefix.Ordering is on you:emit`tools → system → messages`\n\n, stable content first, volatile last.Serialization drift busts the cache silently:re-serializing JSON with different key order, separators, or escaping — or interpolating a timestamp, UUID, or request-ID early in the prompt — changes the bytes even when the meaning doesn't, and the KV states no longer match. Keep the cached prefix byte-identical and push volatile tokens after the last breakpoint.You slide the tail breakpoint yourselfto the newest turn each request to get delta-only writes.Verify any of it with the\n\n`usage`\n\nblock: a non-zero`cache_creation_input_tokens`\n\non the first call, then a non-zero`cache_read_input_tokens`\n\non the next. If both stay near zero across requests that share a prefix, caching isn't engaging.\n\nThis is where understanding turns directly into money. Everything below changes the prefix bytes, so the next request re-keys and **rewrites at 2×** instead of reading at 0.1×. These are the ways it happens **in a real Claude Code session**; a closing **FYI** covers hazards that only apply if you drive the raw API or bolt your own content onto Claude Code.\n\nMCP servers can change their advertised tools at runtime by sending `notifications/tools/list_changed`\n\n. When they do, the new tool definition lands at **byte 0** (tools are first in the render order) → the entire prefix re-keys → a full cold rebuild, now at 2× the write rate.\n\nIt helps to keep three distinct things straight, because only the first one lives at byte 0:\n\n| Thing | Lives in |\n|---|---|\nTool definitions\n|\n`tools` param (byte 0) — changing these is catastrophic |\nTool calls (`tool_use` ) |\nassistant turn content (messages) — late, cheap |\nTool results (`tool_result` ) |\nuser turn content (messages) — late, cheap |\n\nIn one real session the tool count grew **30 → 55 → 85** across consecutive turns with *no model switch*, busting the cache on every turn. The cause: four claude.ai MCP connectors (Zoom, Atlassian Rovo, Microsoft 365 [unauthenticated], Slack) — **remote servers connecting asynchronously at startup.** Each request snapshots whatever tools have registered *so far*, so the byte-0 tool list kept growing as connectors came online mid-session. Pinning the config with `--strict-mcp-config`\n\nfroze it at 30.\n\n⚠️ Mistake — optimizing anything before the tool surface is stable.If tools are still registering across your first few turns, every \"optimization\" you measure is noise on top of a cold rebuild.\n\n✅ Fix— Stabilize the tool/MCP surfacefirst. Use`--strict-mcp-config`\n\n(or a frozen, pinned server set) so byte 0 is constant from turn 1. At a 2× write rate, every cold turn is doubly expensive — this is the highest-leverage fix in the guide.\n\nThese are the things people *expect* to bust the cache — but Claude Code places them so they mostly don't:\n\n`messages[0]`\n\n, it `\"The date has changed\"`\n\nreminder to the newest turn (measured: `cache_read`\n\nheld ~30K, only ~58 tokens written). It re-keys nothing but the sliding tail. `git status`\n\nevery turn and injecting it early would bust constantly — a cautionary design lesson.)`<system-reminder>`\n\nblocksThis case is worth studying because the most popular official platform MCP server made exactly this mistake — and then *deleted the feature.*\n\nVerified against `github/github-mcp-server`\n\n(Go, MIT) at tag ** v1.0.5** (\n\n`1d17d33`\n\n): the README's \"Dynamic Tool Discovery,\" `pkg/github/dynamic_tools.go`\n\n, and the bundled MCP Go SDK (`modelcontextprotocol/go-sdk v1.6.1`\n\n).| Mode | Tools | Mutates byte 0 at runtime? | Cache-safe? |\n|---|---|---|---|\n| default (no flags) |\n`default` toolset, fixed at startup (~50 tools) |\nNo | ✅ |\n`--toolsets repos,issues,…` |\nexplicit groups, fixed at startup | No | ✅ |\n`--dynamic-toolsets` |\n3 meta-tools; starts ~empty, grows on demand |\nYes (`enable_toolset` → `AddTool` → `tools/list_changed` ) |\n❌ |\n\nThe default and explicit `--toolsets`\n\npaths resolve the tool set **once at startup** (`ResolvedEnabledToolsets`\n\n) and never touch it again — a frozen prefix, cache-safe. The `--dynamic-toolsets`\n\nmode (off by default; env `GITHUB_DYNAMIC_TOOLSETS`\n\n) instead starts nearly empty and exposes three meta-tools — `list_available_toolsets`\n\n, `get_toolset_tools`\n\n, `enable_toolset`\n\n— so the model turns groups on as it needs them.\n\nThat convenience is a cache-buster. When the model calls ** enable_toolset**, the handler registers the group's tools against the\n\n`EnableToolset`\n\n→ `RegisterFunc`\n\n→ `s.AddTool`\n\n), and `AddTool`\n\nfires `notifications/tools/list_changed`\n\n. New tool definitions land at byte 0 → the whole prompt prefix re-keys → a full cold rebuild (2× write) on the next turn, paid again on `enable_toolset`\n\ncall.An epilogue, read honestly: **GitHub removed the dynamic-toolsets feature entirely** in v1.1.0 (PR #2512, commit `0f0506d`\n\n, 2026-05-20) — absent from every release since, including v1.3.0. But the maintainers do **not** cite caching or token cost. Their stated reasons are *tech-debt cleanup* — the dynamic path \"carried real complexity: a separate config flag plumbed through stdio + http configs, a parallel registration path, four inventory methods … three meta-tools\" — and that it was \"a path no longer in active use,\" superseded by **client-side** progressive discovery (the removal author explicitly names \"Anthropic's Tool Search Tool, OpenAI's equivalent, 'Code Mode' patterns\"). So this is *consistent with* the frozen-prefix principle, **not proof** that cache cost drove the deletion. The cache-busting cost itself is documented first-party — by **Anthropic**, not GitHub: [Prompt caching](https://platform.claude.com/docs/en/build-with-claude/prompt-caching) states \"Modifying tool definitions … invalidates the entire cache.\" And tellingly, the replacement paradigm — tool search — is *cache-preserving by design*: it **appends** tool schemas inline rather than swapping the prefix, so \"the prefix is untouched, so prompt caching is preserved\" ([tool search docs](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool)). **[removal + stated reasons verified via PR #2512; the cache motive is our analysis, not GitHub's]**\n\n**Live companion — Docker MCP Gateway, the same pattern shipped default-on. [measured against source]** Where GitHub\n\n`docker/mcp-gateway`\n\n, Go, MIT, ~1.5k★) ships it on by default. Verified at tag `v0.43.0`\n\n`4833d8c`\n\n): the `dynamic-tools`\n\nfeature is default-enabled (`cmd/docker-mcp/commands/feature.go`\n\n→ `defaultEnabledFeatures{\"dynamic-tools\": true}`\n\n; the launch blog confirms the meta-tools are \"available to your agent by default\"), exposing `mcp-find`\n\n/ `mcp-add`\n\n/ `mcp-remove`\n\n/ `mcp-config-set`\n\n/ `code-mode`\n\nthat add and remove tools on the `pkg/gateway/reload.go`\n\n(`reloadConfiguration`\n\n) calls `mcpServer.RemoveTools(…)`\n\n+ `AddTool(…)`\n\n, which in the MCP Go SDK (`modelcontextprotocol/go-sdk v1.4.1`\n\n) bottom out in `changeAndNotify(notificationToolListChanged, …)`\n\n→ `notifications/tools/list_changed`\n\n— and Docker's own `TestIntegrationToolListChangeNotifications`\n\nasserts it fires on a live add. So every `mcp-add`\n\n/ `mcp-remove`\n\ncold-rebuilds the whole prefix at the 2× write rate, on each call.**Motive, stated honestly:** Docker argues the feature on **raw token volume**, never caching — its slide deck cites \"335 tools => 209K tokens of tool description / request … \\$1.25/1M tokens\" (`examples/tool_registrations/embeddings.md`\n\n) and the [launch blog](https://www.docker.com/blog/dynamic-mcps-stop-hardcoding-your-agents-world/) warns the context window can \"accumulate hundreds of thousands of tokens of nothing but tool definition.\" The word \"cache\" appears nowhere in its blog or docs; the token-volume cost is Docker's claim, the **prompt-cache** consequence (byte-0 definitions cold-rewrite the cached prefix) is ours. **[runtime mutation + list_changed verified at v0.43.0/4833d8c; cache framing is ours]**\n\n⚠️ Mistake — enabling dynamic/runtime tool discovery for the convenience.Every \"enable this toolset on demand\" call cold-rebuilds your entire prompt prefix.\n\n✅ Fix— Use afrozen startup tool set(default or explicit`--toolsets`\n\n). If you genuinely need many tools, prefer a single fixeddispatchertool (select the operation via arguments) or tool-search thatappendsschemas to the tail rather than mutating byte 0.\n\nThis buster is different from the ones above: it fires from a decision the **model** makes — a big parallel tool fan-out — not from anything you configured.\n\n**The mechanism, in plain terms.** Each turn, the API tries to reuse the cache by matching the prefix at a breakpoint. If that exact match misses, it walks *backward* looking for a still-cached chunk to extend — but only about **20 content blocks**. Find a cached chunk inside that window → the whole prefix is served warm. Find nothing → the API gives up and re-charges the *entire* prefix at the 2× cold-write rate. So when the previous turn appended more than ~20 blocks, last turn's cached chunk is now sitting too far back to reach, and the next turn pays full freight.\n\n**Why tool bursts trip it so easily.** Recall a message holds many blocks, and this window counts **blocks, not messages.** Each parallel tool call is **two** blocks — the `tool_use`\n\nand its `tool_result`\n\n— so a turn with just **~10 parallel tools is already ~20 blocks**, right at the edge. The threshold is razor-sharp: 19 added blocks still re-links, 20 misses (and 20 blocks crammed into a *single* message still misses — it genuinely counts blocks). Measured on `claude-opus-4-8`\n\n:\n\n| one turn's fan-out | blocks added | next turn |\n|---|---|---|\n| 28 parallel tools | 57 |\nMISS — entire prefix re-charged cold (28,149 written, 0 read) |\n| 5 parallel tools | 11 |\nHIT — prefix served warm (25,672 read, 452 written) |\n\nSame setup; only the burst size differs.\n\n**Two facts that make a fix possible.** (1) Re-linking needs just **one breakpoint within ~20 blocks of the last cached chunk** — how far the *end* of the conversation is doesn't matter, and you don't need an unbroken chain of markers. (2) The cache entry isn't deleted when a turn overflows; it lives for the full hour. You only need a marker close enough to *point back at it* on the very next request — so the repair is needed only on the oversized turn itself, not forever after.\n\n**You can't do this inside Claude Code.** The client places the breakpoints for you — and spends only **3 of the 4** the API allows (see *The 3-breakpoint finding* below; the 4th slot sits unused) — with no way to add or move one. A feature request to expose breakpoint placement in `settings.json`\n\nwas filed and **closed as not planned** (\n\n**Behind a proxy you could potentially fix it.** A proxy sitting between Claude Code and the API sees the\n\n`cache_control`\n\nmarkers before forwarding, with no memory of prior calls. And because Claude Code uses only 3 markers, a proxy has the whole budget of | turn | naive (Claude Code's 1 tail marker) | proxy grid (4 markers, stride 18) |\n|---|---|---|\n| +57-block burst | read 0 · write 16,879 ✗ | read 14,794 · write 2,085 ✓ |\n| +31-block burst | read 0 · write 18,011 ✗ | read 16,888 · write 1,123 ✓ |\n\nTwo limits on the proxy fix: four markers at stride 18 rescue a single turn of up to **~74 new blocks** — past that, even the grid can't reach the prior chunk. And the proxy must forward the prefix **byte-for-byte**; re-sorting the tools or re-serializing the JSON busts the cache on its own, markers or not.\n\n⚠️ Mistake — letting an agent fan out 10+ parallel tool calls in one turn on a cached prefix.~20 blocks overflows the lookback, and the next turn cold-rewrites the whole prefix at 2×.\n\n✅ Fix— In Claude Code you can't place your own markers, so bound the burst (fewer parallel calls per turn) — that fixes the common case for free. The proxy grid (4 markers, stride ~18, rescues up to ~74 new blocks) is alast resort, not a default: a burst big enough to overflow the window is rare, and a request-rewriting proxy nowowns your cache correctness— it must forward the prefix byte-for-byte and is still capped at the ~74-block ceiling. Don't stand one up — or buy a \"cache-optimization\" layer that does — without understanding every constraint above; for a problem this infrequent, the added complexity and failure surface rarely pay for themselves.\n\nThe classic prompt-cache killers below **don't come from Claude Code** — it keeps volatile content out of the cached prefix (the date sits after the system breakpoints, the per-request billing-header token is excluded from the cache key, git is snapshotted once). They bite only when you build the prefix **yourself**: calling `/v1/messages`\n\ndirectly, or bolting content onto Claude Code via `--append-system-prompt`\n\n/ `--system-prompt`\n\n, a SessionStart hook or proxy, or a custom MCP server whose tool *descriptions* embed per-request data (those land at byte 0). They're *silent* because the meaning looks unchanged — only the bytes moved:\n\n`datetime.now()`\n\n, UUIDs, or request-IDs interpolated `json.dumps`\n\n(key order drifts between requests).\n\n✅ Fix— Keep per-request tokensafterthe last breakpoint (the message tier), exactly where Claude Code puts them. Verify with`cache_read_input_tokens ≈ 0`\n\nacross requests thatshouldshare a prefix; if reads are zero, diff the rendered bytes of two consecutive requests.\n\nNow we ground the abstract rules in what Claude Code 2.1.150 actually sends on the wire. (This layout is an implementation detail and is version-specific — re-verify per release.)\n\nCaptured via raw request logging (`OTEL_LOG_RAW_API_BODIES=1`\n\n): **3 cache_control breakpoints, all with ttl:\"1h\"** — the 4th available budget slot is unused.\n\n```\nTOOLS: 30 definitions             ← byte 0, no marker of their own\nSYSTEM:\n  system[0] len=85   billing/version header (per-request cch token)\n  system[1] len=62   \"You are a Claude agent…\"        ◀ BREAKPOINT 1 (1h)  — covers tools + identity\n  system[2] len=26887 \"You are an interactive agent…\" ◀ BREAKPOINT 2 (1h)  — full system prompt\nMESSAGES:\n  messages[0].content[0]  <system-reminder> skills list\n  messages[0].content[1]  <system-reminder> context + DATE\n  messages[0].content[2]  user prompt (turn 1)\n  …turns…\n  last user message                                    ◀ BREAKPOINT 3 (1h) — slides each turn\n```\n\nNote that **tools fold into Breakpoint 1** — there is no dedicated tool breakpoint. A multi-turn (`-c`\n\n) capture shows the tail breakpoint (BP3) sliding forward each turn while the two system breakpoints stay put. This is the textbook sliding-window pattern from earlier: frozen system tier written once, sliding tail writing only the delta.\n\n`system[0]`\n\ncontains a `cch=`\n\ntoken that **changes on every request** — yet warm reads still hit. How? The whole of `system[0]`\n\nis **special-cased out of the cache key**, so it never busts Breakpoint 1. The block is the billing/version header — `x-anthropic-billing-header: cc_version=…; cc_entrypoint=…; cch=…;`\n\n— and the exclusion covers the **entire block, not just the volatile cch= token**: changing the version string or the entrypoint is ignored for caching too. (Measured: mutating a non-\n\n`cch`\n\nbyte of `system[0]`\n\nacross a warm turn still reads the full prefix; the `system[1]`\n\ninstead cold-rewrites — so the cache key is live, `system[0]`\n\nis simply outside it.) It's a deliberate exception — a per-request-varying span sitting The date lives in a `<system-reminder>`\n\nin the **first user message** — *after* both system breakpoints. A mid-session midnight rollover is cheaper than you'd expect: it **does not rewrite messages[0]** at all. Verified by holding one session alive across Pacific midnight and capturing the wire — turn 1 (23:48, 06-16) and turn 2 (00:01, 06-17, same session):\n\n| turn |\n`messages[0]` date |\nrollover notice | usage |\n|---|---|---|---|\n| 1 (before) | `2026-06-16` |\n— | read 21,812 · write 8,305 |\n| 2 (after) | still `2026-06-16` |\n`\"The date has changed. Today's date is now 2026-06-17\"` appended to the new user turn (msg tail) |\nread 30,117 · write 58\n|\n\nSo the harness leaves the original date in `messages[0]`\n\nuntouched and **appends a \"date has changed\" reminder to the sliding tail** — re-keying only the cheapest tier (≈58 tokens written, the whole prefix still read warm). It doesn't touch the system tier\n\n`messages[0]`\n\n. Git status is snapshotted once at session start (frozen). Transcripts don't persist the `cache_control`\n\nmarkers — capture the wire request, not the transcript.\n\n⚠️ Mistake — trusting the displayed session cost as your true bill.Claude Code's`total_cost_usd`\n\nprices its 1-hour writes at the 5-minute1.25×rate, so it readslowerthan your actual invoice.\n\n✅ Fix— Use the displayed cost forrelativecomparisons within a session, but treat theAnthropic Consoleas authoritative for absolute billing. (Act IV quantifies the gap on a real 16-turn session: $1.77 displayed vs $1.97 actual.)\n\n**What to do (end of Act I):** Keep the tool/MCP surface frozen from turn 1; keep volatile tokens (dates, IDs) *after* the last breakpoint; verify caching with the `usage`\n\nblock, not faith; and don't trust the displayed cost as your invoice. With that, the steady-state cost of a single-model session is mostly cheap reads plus the output you generate. The expensive surprises come from the two things in Act II.", "url": "https://wpnews.pro/news/claude-code-costs-act-i-how-the-billing-actually-works", "canonical_source": "https://dev.to/sumedhbala/claude-code-costs-act-i-how-the-billing-actually-works-25kn", "published_at": "2026-06-26 05:58:50+00:00", "updated_at": "2026-06-26 06:03:47.744728+00:00", "lang": "en", "topics": ["large-language-models", "ai-tools", "developer-tools"], "entities": ["Claude Code", "Anthropic", "Sonnet 4.6", "Opus 4.7", "Opus 4.8", "Haiku 4.5"], "alternates": {"html": "https://wpnews.pro/news/claude-code-costs-act-i-how-the-billing-actually-works", "markdown": "https://wpnews.pro/news/claude-code-costs-act-i-how-the-billing-actually-works.md", "text": "https://wpnews.pro/news/claude-code-costs-act-i-how-the-billing-actually-works.txt", "jsonld": "https://wpnews.pro/news/claude-code-costs-act-i-how-the-billing-actually-works.jsonld"}}