Skillforge Field notes on shipping with AI tools

Claude Code Prompt Caching: What Breaks It and the TTL Fix

claude-codecosts

Every turn in Claude Code re-sends the whole conversation: system prompt, CLAUDE.md, every earlier message and tool result, then your new message. Prompt caching is why that does not cost the full price every time. The API matches the start of the request against what it processed recently, bills the matched part at about a tenth of the input rate, and fully processes only what changed.

Which means the interesting question is not "is caching on" (it is, by default, everywhere). It is "what did I just do that made the next turn slow and expensive". This guide lists those actions, puts numbers on the cost, and covers the two settings that shipped this week, promptCacheTtl and subagentPromptCacheTtl, which let API-key users control how long the cache lives. Everything here is checked against the Claude Code docs (Prompt caching, Settings reference, Environment variables, Changelog) and the Claude API pricing page on August 27, 2026.

The short answer

  1. The cache is one prefix match. Anything that changes content near the top of the request (model, effort level, tool definitions, system prompt) recomputes everything after it. Anything appended at the end is cheap.
  2. Eight actions invalidate it: switching model, changing effort, turning on fast mode, an MCP server connecting or disconnecting (only when its tools are loaded into the prefix), enabling or disabling a plugin that ships an MCP server, denying an entire tool, /compact, and upgrading Claude Code.
  3. On an API key or cloud provider, the cache lives five minutes. On a subscription within plan usage, the main conversation gets an hour. To get the hour on an API key, set "promptCacheTtl": "1h" (Claude Code v2.1.242 or later per the docs; the changelog lists it under 2.1.243, August 25, 2026).
  4. Watch it with a status line that prints cache_read_input_tokens against cache_creation_input_tokens. If creation stays high turn after turn, something in your prefix is moving.

How the cache is keyed

Claude Code orders each request so the parts that rarely change come first:

LayerContentChanges when
System promptCore instructions, tool definitions, output styleThe set of loaded tool definitions changes, or Claude Code is upgraded
Project contextCLAUDE.md, auto memory, unscoped rulesSession start, /clear, /compact
ConversationYour messages, Claude's replies, tool resultsEvery turn

A change in the conversation layer leaves the two layers above it cached. A change in the system prompt layer invalidates everything below, because all of it now sits behind a different prefix. There is no per-file or per-segment caching; the match is exact from the first byte.

Two things are part of the cache key without being part of the prompt text: the model and the effort level. Each combination has its own cache. Switching either mid-session means the next request reads the entire history with zero cache hits, even though not a character of it changed.

The eight actions that invalidate the cache

Each of these produces one slower, more expensive turn, after which the new prefix is cached and you are back to normal.

Switching models. /model starts a fresh cache. Since v2.1.238 Claude Code asks you to confirm the switch only while the cache is still warm, meaning within one TTL of the last request; if the cache has already expired there is nothing to lose and it switches silently. Two model switches are easy to miss: the opusplan setting flips between Opus and Sonnet every time you enter or leave plan mode, and automatic model fallback on Fable 5 and Opus 5 (when a safety classifier routes a request to a fallback model) is also a switch.

Changing effort level. Same rule, same confirmation. A change that resolves to the level already in effect keeps the cache.

Turning on fast mode. Fast mode adds a request header that is part of the cache key, so the first fast turn re-reads the whole history, billed at fast mode rates. This is a once-per-conversation cost: after that first turn, Claude Code keeps sending the header and only varies the speed setting, which is not in the key. Turning fast mode off, the automatic fallback to standard speed after a rate limit, and turning it back on later all keep the cache. Enabling fast mode from a non-Opus model is additionally a model switch.

An MCP server connecting or disconnecting. Tool definitions sit in the system prompt layer, so a changing tool set invalidates the cache. Whether an MCP change does that depends on whether the server's tools are deferred by tool search (the default on supported models) or loaded into the prefix. Deferred tools only append. Tools land in the prefix when tool search is unavailable or disabled, when a server or tool is marked alwaysLoad, or when threshold-based loading keeps definitions upfront, and then the usual culprit is not you: a stdio server process exits, an HTTP session expires, or a server reconnects after a transient failure. Editing your MCP config does nothing until restart.

Enabling or disabling a plugin. Only plugins that provide MCP servers can invalidate, under the same deferred-versus-prefix rule. Skills, commands, agents, hooks, monitors, and themes append and never invalidate. The cost lands when the change applies (/reload-plugins or a new session), not when you run /plugin enable; if a reload would force a full re-read, Claude Code warns and refuses unless you pass --force.

Denying an entire tool. A bare tool name in permissions.deny, such as Bash or WebFetch, removes that tool's definition from the system prompt, so adding or removing such a rule mid-session invalidates. Scoped rules like Bash(rm *), and all allow and ask rules, are checked at call time and leave the prefix alone. This matters if you tune permissions during a session, which is exactly what people do after a Blocked by classifier message in auto mode.

Compacting. /compact replaces the history with a summary, so the conversation layer cannot match the old prefix, by design. The part worth knowing is what the summarization request itself costs: while the cache is warm it reads your prefix from cache; after a break longer than the TTL it reprocesses the full history uncached. So /compact is cheapest mid-session and most expensive when you resume an old session and compact first.

Upgrading Claude Code. A new version typically changes the system prompt or tool definitions. Auto-update applies on the next launch, never mid-session, so this is one uncached first turn after a restart. Resuming a long session after an upgrade reprocesses the entire history with no hits, which can be the single most expensive request you send. DISABLE_AUTOUPDATER=1 lets you choose when that happens.

Things that look dangerous but keep the cache

  • Editing files in the repo. File contents enter context only when Claude reads them, and reads append. Claude Code adds a change notice and re-reads.
  • Editing CLAUDE.md mid-session. Root and user CLAUDE.md are read once at session start and held in memory. The edit does not invalidate the cache, and it also does not apply until /clear, /compact, or a restart. This is the answer to "I changed CLAUDE.md and Claude ignored it." Nested CLAUDE.md files and paths: rules load lazily, so edits before first load do take effect.
  • Changing output style. Same shape: read at start, no invalidation, no effect until restart.
  • Changing permission mode. Modes do not touch the system prompt. The one exception is plan mode under opusplan, because that is a model switch.
  • Invoking skills and commands. Their instructions inject as user messages at the point of use.
  • /recap. Appends a summary as command output instead of replacing history, unlike /compact.
  • /rewind. Truncates back to a prefix that is already cached, and every turn since has been reading through that prefix, so it is warm.
  • /cd. Added in 2.1.169 specifically to move a session to a new working directory without breaking the cache.
  • Spawning a subagent. From the parent's side, the call and its result append. The subagent builds its own separate cache (more below).

What a miss costs

The API prices caching as multipliers on the base input rate: 5-minute cache writes at 1.25x, 1-hour writes at 2x, cache reads at 0.1x. For the current models, per million tokens:

ModelBase input5m write1h writeCache read
Fable 5$10$12.50$20$1
Opus 5$5$6.25$10$0.50
Sonnet 5$2$2.50$4$0.20
Haiku 4.5$1$1.25$2$0.10

Take a 150,000-token session on Opus 5. A turn that reads it all from cache pays 150,000 x $0.50 / 1M = $0.075 for the history. The turn after a model switch pays 150,000 x $5 / 1M = $0.75 to process it, plus the write to cache it again. Ten times the price for the same tokens, on top of a slower response.

On a subscription you do not see dollars, but the same tokens count against plan usage. /usage on Pro, Max, Team, and Enterprise plans flags any behavior that accounts for 10% or more of recent usage, and "cache misses" is one of the named flags. Pro users also get a footer hint (since 2.1.92) when returning to a session after the cache expired, estimating how many tokens the next turn will send uncached.

Cache lifetime and the new TTL settings

The cache expires after a period of inactivity, and every hit resets the timer, so it stays warm as long as you keep working. The API offers two lifetimes: five minutes, and one hour at the higher write rate. Claude Code decides per request, and every request falls into one of two buckets:

  • Main conversation: your interactive turns, -p runs, Agent SDK turns, and the helpers that run inline with them.
  • Everything else: subagents, workflows, in-process teammates, forks, compaction, session titles.
BucketSubscription, within plan usageUsage credits, API key, or cloud provider
Main conversationOne hourFive minutes
Everything elseFive minutes (a small set of server-controlled helper requests get one hour)Five minutes

Once a subscriber crosses their plan limit and starts drawing on usage credits, they are being billed for tokens, so Claude Code drops the main conversation to the cheaper five-minute TTL.

Until this week, an API-key user who wanted the hour had one blunt tool: ENABLE_PROMPT_CACHING_1H=1 (added in 2.1.108), which requests an hour on every request including subagents. The two new settings split that by bucket. Each takes exactly "5m" or "1h"; any other value is ignored.

{
  "promptCacheTtl": "1h",
  "subagentPromptCacheTtl": "5m"
}

That is the configuration the changelog describes as the point of the change: a long-lived cache on the conversation you keep coming back to, and the cheaper write rate on subagents that finish in minutes and are never revisited. Both keys go in any settings file (user, project, local, or managed). The matching environment variables are CLAUDE_CODE_PROMPT_CACHE_TTL and CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL.

When more than one control applies, the first match wins:

  1. FORCE_PROMPT_CACHING_5M=1 forces five minutes on both buckets
  2. The bucket's environment variable
  3. The bucket's setting
  4. ENABLE_PROMPT_CACHING_1H=1 requests an hour on both buckets
  5. The bucket's default from the table above

FORCE_PROMPT_CACHING_5M exists for debugging cache behavior and for overriding a longer TTL that managed settings imposed. The one-hour TTL is not available through the Claude apps gateway, and on Amazon Bedrock its availability varies by model.

When is the hour worth 2x on writes? The premium applies only to tokens written on each turn, which in a normal session is the new content appended since the last turn. A miss re-reads the whole context at the full rate. So for a long session you step away from for more than five minutes even once, the hour pays for itself the first time you come back. For short bursts that never idle past five minutes, you pay the higher write rate on every turn for a lifetime you never use. The docs say the same thing without the arithmetic.

Measure it with a status line

The two numbers that tell you whether caching is working arrive on every API response, and the status line receives them in context_window.current_usage. This script prints the latest turn's hit ratio next to the model and context percentage. We tested it against the sample JSON in the status line docs.

#!/usr/bin/env node
// Claude Code status line: model, context %, and the cache hit ratio of the
// latest API turn. Reads the session JSON Claude Code pipes to stdin.
let raw = '';
process.stdin.on('data', (c) => (raw += c));
process.stdin.on('end', () => {
  const d = JSON.parse(raw);
  const model = (d.model && d.model.display_name) || 'model';
  const cw = d.context_window || {};
  const pct = cw.used_percentage == null ? '?' : cw.used_percentage;
  const u = cw.current_usage;
  let cache = 'cache n/a';
  if (u) {
    const read = u.cache_read_input_tokens || 0;
    const write = u.cache_creation_input_tokens || 0;
    const fresh = u.input_tokens || 0;
    const total = read + write + fresh;
    const hit = total ? Math.round((100 * read) / total) : 0;
    cache = `cache ${hit}% hit (read ${read} / write ${write} / fresh ${fresh})`;
  }
  process.stdout.write(`[${model}] ${pct}% ctx | ${cache}`);
});

Save it as ~/.claude/cache-statusline.js and point Claude Code at it:

{
  "statusLine": {
    "type": "command",
    "command": "node ~/.claude/cache-statusline.js"
  }
}

On the docs' sample payload it prints [Opus] 8% ctx | cache 13% hit (read 2000 / write 5000 / fresh 8500). current_usage is null before the first API call and again right after /compact until the next call repopulates it, which the script prints as cache n/a. A healthy steady state is a high read count and a small write count each turn; write staying large turn after turn means the prefix is changing, and the list above is where to look.

Subagents, forks, and workflows

A subagent starts its own conversation with its own system prompt and tool set. Its first request cannot read the parent's cache because the prefixes differ, and it warms a cache of its own. It also sits in the "everything else" bucket, so it gets five minutes even on a subscription unless you set subagentPromptCacheTtl. A fork is different: it inherits the parent's system prompt, tools, and history exactly, so its first request reads the parent's cache.

Dynamic workflows add one mechanism of their own. When a fan-out starts several agents with the same model, effort, agent type, tools, output schema, and working directory, Claude Code holds all but the first until the first agent's response begins, then releases the rest so their first requests read the prefix the first one cached. The hold is capped by CLAUDE_CODE_WORKFLOW_PREFIX_STAGGER_MS (default 5000, from v2.1.229; 0 disables it).

Two rules for unattended runs

If you run Claude Code on a schedule with an API key, every request defaults to the five-minute TTL. So a job that does a burst of work, waits ten minutes on something, and continues pays a full re-read when it continues; "promptCacheTtl": "1h" in the settings the run uses is the fix, at the 2x write premium on the tokens each turn appends.

And the cache is scoped tighter than you might expect: the system prompt embeds the working directory, platform, shell, OS version, and auto memory paths, so sessions in different directories (including worktrees of the same repository) build different prefixes, and sequential sessions in one directory share a prefix only when the git status snapshot at startup matches. A fleet of runners will not warm each other's cache unless you use the Agent SDK's option to suppress the per-machine sections of the system prompt.

FAQ

Why is the first message after a break so slow?

The cache expired. Each hit resets the timer, but once the gap exceeds the TTL (five minutes on an API key, an hour on a subscription within plan usage) there is nothing to match, so the next request recomputes the full input and re-establishes the cache. On Pro or Max, Claude Code offers to resume a large session from a summary in this situation so later turns carry less history.

Does editing CLAUDE.md invalidate the cache?

No, and the edit does not take effect either. Root and user-level CLAUDE.md are read once at session start and held in memory. The new content loads on the next /clear, /compact, or restart. If you need the change now, /compact at a natural break is the cheapest way to pick it up, because a warm-cache compaction reads your prefix from cache.

How do I turn prompt caching off?

Set DISABLE_PROMPT_CACHING=1, or one of the per-model variants (DISABLE_PROMPT_CACHING_FABLE, _OPUS, _SONNET, _HAIKU). The docs frame this as occasionally useful for debugging a specific model or provider, and for normal use say to leave it on. Organizations can put these or the TTL variables in the env block of managed settings.

Does promptCacheTtl do anything on a subscription?

Within plan usage, the main conversation already gets an hour, so setting "1h" changes nothing there. It matters once you are drawing on usage credits, when Claude Code otherwise drops to five minutes, and subagentPromptCacheTtl matters on every plan because subagents default to five minutes everywhere.