Back to blog
Jul 24, 2026
14 min read

How Claude Code talks to the API

The hidden API traffic driving your coding agent: the 9 request kinds, Claude's explicit cache system, the 5-minute TTL, and why most token-saving tricks fail.

When you type a prompt into Claude Code, you trigger a single API call. At least, that’s what it looks like. Behind the scenes, Claude Code is anything but quiet. The agent, its subagents, and the CLI itself generate a constant stream of background API traffic: file searches, session summaries, title generation, context compaction. All calls you never asked for and never see.

After profiling 160 sessions and 6,767 requests through ai-agent-profiler, here is where the money actually goes (hover rows for detail):

Kind% of cost% of tokensTriggered by
main42%38%Your prompt
search31%35%File-search subagent
compact12%14%Context compaction
recap6%5%Idle-time summary
subagent4%4%Other subagents
title2%1%Session title gen
webfetch2%2%Web fetch summary
guide1%1%Claude guide agent

More than half your API bill comes from calls you never made. search alone rivals your actual turns. This is the hidden machinery of a coding agent. Understanding it is the first step to controlling it.

The real message loop

When you hit Enter in Claude Code, the quiet period ends immediately:

user messagemainuser turn → responseresponsetext + tool callssearch subagentsearch (x3-8 calls)explore codebasesearch resultsfile readsgrep → glob → readedit / writeapply changesbackground: compact (x1) + title (x1) + recap (x0-1)invisible to you, on your billnext user message

A single user turn typically generates 4-14 API calls, only one of which is your actual prompt. The rest are automated by the agent.

The tool loop

But that is not the whole story. When the model decides to use a tool like Read, Grep, or Bash, each tool call triggers a new API request. The cycle goes:

prompt: “fix the bug”tells Claudeto Grep for “auth”Claude Code runsgrep locallysends grep resultsback as API requestgot 3 matches,tells Claude to read onereads the file,writes a patchapplies edit,sends back resultdone, sends final replyAPI requestAPI requestAPI requestAPI request

A task like “find where authentication logic lives and explain it” can chain 6-8 tool calls in sequence: search for the module, read the file, read a related config file, read the middleware, then compose the answer. Each tool round is a paid API request.

This loop is not background subagent traffic. The tool loop is the model working on your request, going back and forth between issuing a tool use and consuming the tool result. Subagents are separate processes the CLI spawns for delegated tasks, and they run their own independent tool loops.

From the API’s perspective the two look identical. Both send the full conversation prefix each turn with a new tool result appended. You can only tell them apart by classifying who initiated the request: the main agent working on your prompt, or a subagent spawned by the CLI.

The 9 request kinds

ai-agent-profiler tags every API call with a kind, classifying who made it and why, so you can separate “cost I asked for” from “overhead the tool spent on my behalf.”

KindModelUser?Description
mainprimaryyesYour interactive turns, the core conversation loop
searchprimarynoFile-search / Explore subagent navigating the codebase. Usually the largest non-user bucket.
compactprimarynoContext compaction, full-history summarisation to shrink the context window
recapprimarynoMid-session catch-up summary injected when you return to an idle session (“The user stepped away…“)
subagentprimarynoA subagent we couldn’t further identify (fallback)
titlesmall/fastnoSession-title generation (3-7 words), a one-shot Haiku call
webfetchprimarynoSubagent summarising fetched web-page content
guideprimarynoClaude guide agent answering “how do I…” about Claude Code/SDK/API
quotanoUsage-limit check (minimal cost)

How the classifier works

Detection uses two independent signal sources, deliberately kept separate to avoid false positives.

Signal 1: The system prompt. Claude Code prepends an x-anthropic-billing-header system block to every call. Subagents set cc_is_subagent=true and carry a specialist identity in the same block:

x-anthropic-billing-header: cc_version=2.1.209; cc_entrypoint=cli; cc_is_subagent=true;
You are a file search specialist for Claude Code…      ← classified as "search"

OpenCode agents carry their identity in per-agent system prompts (prompt/title.txt, prompt/compaction.txt, prompt/explore.txt). The classifier is provider-agnostic. It checks both Anthropic’s top-level system field and OpenAI-style role:"system" messages, accumulating both before classification.

Signal 2: The last message text. Some calls run on the main model with a normal system prompt. Nothing in the system block distinguishes them from a user turn. recap, some compact calls, and webfetch are only identifiable by their final instruction:

KindMarker in last message
recap"The user stepped away" + "recap"
compact"summary of the conversation" / "create a detailed summary"
webfetch(subagent +) "Web page content:"
title (OpenCode)"Generate a title for this conversation:"
raw requestcheck system promptcc_is_subagent=true → search, guide”title generator” → title”quota” → quotamatched?tnocheck last message text”The user stepped away” → recap”summary of the conv…” → compact”Web page content:” → webfetchstill no match → main

False positive traps

Two subtle traps were hit and fixed during development:

  1. Match the last message only, not the whole transcript. A prior summary gets echoed into later requests’ context. Matching anywhere in history would mislabel every subsequent normal turn as compact.

  2. Read text blocks only, not tool results. tool_result content gets JSON-stringified, so captured command output mentioning “summary of the conversation” would leak in. The classifier reads only type: "text" blocks, so tool output can’t trigger a match.

Both effects are real: an earlier system-prompt-only classifier folded all recaps into main and produced ~100 false compact hits. The current rules reduce that to zero.

Where does the money actually go?

main 42%search 31%compact 12%recap 6%sub 4%other 5%main, your actual turnssearch, file-search subagentcompact, context compactionrecap, idle-time summarysubagent, other agentstitle / webfetch / guide / quota58% of costs are non-user callssearch alone matches your turns

58% of your API bill comes from calls you never made. search rivals your actual turns in cost. The agent needs this, but it is opaque by default. The classifier makes it visible.

How Claude’s cache actually works

Claude’s cache is explicit: the client must opt in by placing cache_control markers in the request body. Without them, nothing is cached.

{
  "system": [
    {"type": "text", "text": "Short preamble"},
    {"type": "text", "text": "Main instructions...",
      "cache_control": {"type": "ephemeral"}}
  ],
  "tools": [...],
  "messages": [
    ...,
    {"role": "user", "content": [
      {"type": "tool_result", "content": "...",
        "cache_control": {"type": "ephemeral"}}
    ]}
  ]
}

Each marker is a breakpoint. The API caches everything from position 0 up to each breakpoint, keyed by the exact byte sequence of the serialized request (system, then tools, then messages). Any change to any byte before a breakpoint invalidates that entry: reordering, whitespace, a single character in a tool description, or editing a past message.

Where Claude Code places breakpoints

Claude Code places exactly 3 breakpoints (verified across all captured Bedrock traces):

  1. system[1]: after a short preamble
  2. system[2]: after the full system prompt
  3. Last user message: on the most recent tool_result block
systemtoolsmessages (all past turns)preamblefull system prompttool definitionsturns 1..N-1BP1BP2BP3cachedcached (system + tools)cached (whole prefix)new

The economics

Token typeCost per MTok (Opus 4.x)Ratio
Cache read$0.500.1x input
Cache write (5 min)$6.251.25x input
Cache write (1 hour)$10.002x input
Regular input$5.001x
Output$25.005x

A cache write is 12.5x a cache read. Converting reads to writes by editing a cached prefix hurts: 100K tokens of cache-edit converts $0.05 worth of reads into $0.575 of writes. This one fact defeats nearly every prompt-optimization strategy.

The minimum cacheable block is ~2048 tokens. Content smaller than this is billed as regular input even with cache_control markers present. Claude Code’s system prompt alone exceeds this threshold many times over.

The 5-minute TTL and the 1-hour upgrade

Cache entries expire after inactivity. Anthropic states “at least 5 minutes” for the ephemeral type. Claude Code always sends the 5-minute TTL by default; every captured cache_control marker is bare {"type":"ephemeral"} with no ttl field.

ai-agent-profiler can upgrade these to 1 hour:

aap run claude --cache-1h

The proxy rewrites every ephemeral cache_control marker to { type: "ephemeral", ttl: "1h" } before forwarding upstream. This is a pure cache-lifecycle operation: no prompt shrinking, no editing. One-time write penalty, fully reproducible.

TTLWrite costRead costSurvives idle gaps of
5 min (default)1.25x input ($6.25)0.1x input ($0.50)< 5 min
1 hour (upgraded)2x input ($10.00)0.1x input ($0.50)5 min – 1 hour

The 1h upgrade costs 2x on writes but survives 12x longer. It wins when idle gaps often fall between 5 minutes and 1 hour, exactly the pattern of real coding sessions where you read docs, think, or switch contexts between turns.

Keep-alive pings

Preventing cache expiry during idle by replaying the last request with max_tokens: 1:

Cache TTLPing intervalPings to break even
5 min4 min~12.5
1 hour48 min~20

On the 5-minute cache, keep-alive barely breaks even (~12.5 pings in 56 min ≈ one rebuild). On the 1-hour cache, it becomes dramatically cheaper: 20 pings at ~$0.10 each vs ~$1.25 per rebuild. Break-even ≈ 12 hours of idle.

However, keep-alive has caveats: it bets the user returns (wasted if they don’t), generates phantom API calls the user never issued (real cost, real quota usage), and breaks the proxy’s transparency (no longer a passive pipe). It’s designed but kept as a future opt-in, gated behind the 1h cache.

Cross-session cache warming: why benchmarks lied

The cache key is the byte prefix, not the session ID. The cache persists across sessions: if two requests from different sessions share the same prefix, the second one gets a cache read instead of a write.

Session A (baseline, cold)[system + 9 tools + msg1]WRITE $6.25Session B (baseline, warm)[system + 9 tools + msg1] ← same prefix!READ $0.50Session C (optimized, cold)[system + 6 tools + msg1] ← different prefix!WRITE $6.25Session B gets cheap reads because A already paid the write. Session C starts cold and looks worse.

This means:

  1. Repeated baseline runs get free reads warmed by earlier baselines, making them look artificially cheap.
  2. First optimized runs start cold (different prefix), paying full write cost.
  3. Early benchmarks were impressive and wrong. The cost model initially did not count cache-write tokens at all, so it under-reported exactly the cost that prefix-editing creates.

The fix: fair comparisons must warm both arms equally (discard the first run of each), or compare cold-vs-cold after waiting for TTL expiry. The profiler’s benchmark runner now requires this.

Why most optimization strategies fail

The optimization layer attempted to shrink prompts by pruning stale results, collapsing the system prompt, removing unused tools, compacting history, and reordering content. Each strategy reduces prompt size but changes bytes before a cache boundary.

On Anthropic/Bedrock, this turns cheap cache reads into expensive cache writes. On DeepSeek/OpenAI, it re-bills the downstream tail at the miss rate (~10x the hit rate). Either way: shrinking a prompt that was already caching cheaply trades pennies of saved reads for dollars of new writes.

Before optimization:system + toolsmessagesnew tailall cached READ ($0.50/MTok)After prefix edit (prune 3 tools → WRITE at changed region):system6 tools (edited!)messages (now MISS)new tailedit point → everything after is WRITE ($6.25/MTok = 12.5x more expensive)

The math on this is harsh. For an edit at position P affecting D downstream tokens:

savings  =  R removed tokens × cheap read rate
cost     =  D downstream tokens × (write rate − read rate)
net      =  savings − cost

On Anthropic with a 5m cache, you’d need to remove roughly 90% of the downstream tail to break even. In practice, coding agent prefixes are massive and the budget for “edits that save” is close to zero.

What actually survives the purge

The layer keeps only the edits that don’t disturb the cached prefix:

  • stripTools: remove tool defs from turn 1 (stable prefix from the start)
  • stableTruncate: deterministic head+tail truncation, re-applied identically to every matching result on every turn
  • shapeTestOutput: strip passing-test spam and ANSI from test output, deterministically
  • upgradeCacheTtl: 5m to 1h cache lifetime extension (no prompt editing)

Everything that edits the middle of the prompt, like pruneStale, collapseSystem, pruneUnusedTools, frozenCompact, is disabled automatically per provider via the cache-family mapping. The default posture is: profile, don’t rewrite.

The double-write trap with optimizeOnCold

A natural idea: “Once the cache has expired, the next request pays a full write anyway, so apply all optimizations for that one turn to shrink the write.” We implemented this. It does not work.

Cold turn N:    collapse system S→S', prune history M→M', write S'+T+M'
Turn N+1:       layer reverts to steady-state, edits are OFF
                client re-sends pristine S+T+M (never knew we edited it)
                cache holds S', request sends S → divergence at byte 0
                ENTIRE prefix rebuilds — second full write

Net result: two writes instead of one. The only way to avoid this is to apply the same edit identically on every subsequent turn, which rules out one-shot cold rewrites entirely. optimizeOnCold is left in the code, configurable, default OFF.

Two cache families, two failure modes

FamilyProvidersMechanismConsequence of prefix edit
explicitAnthropic, BedrockClient-placed cache_control breakpointsCheap reads become expensive writes (~12.5x)
prefixDeepSeek, OpenAI-compatibleAutomatic longest-common token prefixDownstream tail re-billed at miss rate (~10x hit)

Same conclusion: editing the cached prefix loses. The profiler encodes this by mapping each provider to its cache family, then disabling the prefix-editing strategies for that family automatically. No manual tuning needed.

What this means for you

If you’re using a coding agent with Claude, here’s what the data says:

  1. You are paying 2x what you think. More than half your bill is background traffic: searches, compactions, recaps, title generation.

  2. Cache beats optimization. On Anthropic’s explicit cache, the native caching is already close to optimal. Attempts to shrink prompts by editing history backfire because the cache-read penalty is so high. The biggest lever is cache lifetime, not prompt size.

  3. 1h cache is the most cost-effective single change. aap run claude --cache-1h upgrades Claude’s default 5-minute TTL to 1 hour. One-time 2x write cost, no per-turn edits, fully reproducible. Wins whenever idle gaps exceed 5 minutes.

  4. Measure, don’t assume. The cache key is the byte prefix, not the session ID. Cross-session warming makes naive benchmarks lie. Fair comparisons require warm-up runs or cold-vs-cold methodology. The profiler’s benchmark runner enforces this.

  5. Profile first, rewrite later. Most optimization strategies are disabled by default because the cache math doesn’t favor them. The profiler’s default posture is to make costs visible and let you decide. When rewrite strategies do help, they must be deterministic, reproducible on every turn, and never touch the cached prefix.


All findings are from 160 real coding sessions tracked through ai-agent-profiler. The proxy is open-source (MIT). Run it against your own sessions to see your actual cost breakdown.