What Background Agent Status Summaries Cost: Attributing Model Calls by query_source
A gist that did the rounds on Hacker News at the end of October reports that Claude Code sends each actively working background agent’s transcript back to the model roughly every 30 seconds in interactive mode to produce a three-to-five-word status line, and that in the author’s own data those calls came to 16.4% of estimated spend and 19.7% of API requests over a week. It is one person’s measurement from their own OpenTelemetry export; the tested versions of the interception method span 2.1.257 to 2.1.312. The result needs verification before being applied to other versions or setups.
To estimate this overhead in a workflow, classify model requests by purpose and calculate request counts, tokens and estimated cost separately, then compare them with billing where available. Shares from different periods need comparable workloads before they can be interpreted as a change in cost. The synthetic log below demonstrates the arithmetic and the effect of changing the denominator.
Two earlier posts here, Estimating AI prototypes in two stages and Counting the cost of AI code review, are cited as discussions of total AI workflow cost; their contents could not be retrieved and need verification. This post splits the total by purpose.
1. What the public measurement says
The gist is titled “Claude Code burns ~15–20% of your tokens on a 3-word status line”, by flamerix-ai, tested on Linux with Claude Code 2.1.257 to 2.1.312. The behaviour described: about every 30 seconds, each actively working background agent in interactive mode is asked to “describe your most recent action in 3-5 words”, and each call sends that agent’s full transcript to the model the agent itself uses. The author describes the answer as only updating the status line in the agents panel; whether other workflows also consume it needs verification. The author says it has been on by default for actively working background agents in interactive mode since 2.1.232, that the interval is tunable from the server side within 30 to 240 seconds, and that the default is already the minimum.
The numbers are a before-and-after table, “the week before the hook” against “2 days after”:
- Share of all Claude Code estimated spend: 16.4% before (the main conversation was 15.7%), 0% after.
- Share of all API requests: 19.7% before, 0% after.
- Input to output ratio on those calls about 9,000 to 1, with 96% of the spend on Opus.
- Weekly share over time: 3.5% to 7% in July, 7.3% and then 11.8% after picking up 2.1.233, about 16% by mid-October.
The report has several limits. Spend is Claude Code’s own per-request estimate, the cost_usd field, not an invoice; the effect on subscription limits and actual spending depends on the plan. The author also reports finding these requests in the OpenTelemetry export but not in the on-disk session logs. The author found them by grouping claude_code.api_request events by query_source, at which point agent_summary turned out to cost more than the main conversation.
The gist proposes a Bun --preload script that wraps only the global setTimeout. If a callback’s source text contains an anchor such as “[AgentSummary] Timer fired”, it returns an inert timer with a delay of 2³¹−1 ms; every other timer passes through. The Claude Code binary is not modified. Installation is BUN_OPTIONS=--preload=<path> followed by claude daemon stop --any. The author lists the failure mode plainly: it fails open. Matching depends on source text, so a renamed log line, a moved function or a timer that no longer goes through the global setTimeout brings the summaries back silently, which is why the gist insists on a verification step. This is an unofficial interception method. The author reported no dedicated setting at the time; the current behaviour needs verification. The author also says CLAUDE_CODE_FORK_SUBAGENT=0 and CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1 stop it, at the price of losing fork subagents or background agents altogether.
The SDK path is handled separately in the gist. Through the SDK, summaries can be enabled with agentProgressSummaries: true or the CLAUDE_CODE_AGENT_PROGRESS_SUMMARIES environment variable, and they then arrive as task_progress stream-json events. If anything in your automation consumes that event, find out before switching summaries off. The author did not test ACP adapters; the current SDK behaviour also needs verification.
The gist points to GitHub issue #86879 as collecting four independent measurements in the 18% to 26% range. Opening the issue, the body describes one four-hour session on 2.1.233 on macOS, 27 subagents, 1,496 requests, with agent_summary at 22.7% of requests, 22.2% of raw tokens and 18.0% of tokens weighted by rate. A comment below it reports a 1 hour 45 minute interactive session on 2.1.276 on Linux, and its figure is “26% of the session’s cache-read tokens”. So the two ends of that range use different denominators, weighted tokens at one end and cache-read tokens at the other, with a request share in between, and different versions and session lengths as well. These measures cannot be combined into a general cost range.
The HN snapshot used here records 562 points and 227 comments, but its capture time needs verification. One commenter interprets the code as making a temporary fork of the conversation, allowing prompt-cache use, every 30 seconds. Another, on a 20x Max plan, estimated a share around 16% using the 30-second timer. These figures are user reports; whether they reconcile with billing needs verification.
2. Settle the unit of account first
The first decision when measuring this yourself is what counts as one unit. The sensible unit is one model request, one claude_code.api_request event, classified by what it was for. Tool executions and messages between agents do not map one-to-one onto requests, so mixing tool-call or message counts with request counts makes the units inconsistent and may double-count activity.
The second decision is to keep four quantities apart and never substitute one for another:
- Request count measures frequency. A summary that fires every 30 seconds may account for many requests, depending on how often the rest of the workload calls the model.
- Token count splits further into
input_tokens,output_tokens,cache_read_tokensandcache_creation_tokens. If a summary sends the full transcript and largely hits the cache, its raw token share may be high while its cost share is lower. That depends on cache use and the applicable rates. - Estimated cost, the
cost_usdfield, is the tool’s estimate at model rates. Different models and cache hits or misses pull it away from the token count. - Actual spending depends on the payment model. Usage-based API charges and subscription limits cannot be inferred directly from the same estimated-cost share.
The issue numbers above are the worked example: 22.7% of requests, 22.2% of raw tokens, 18.0% of weighted tokens, reported for the same session, with different denominators.
Decide how failures and retries are counted as well. Whether a retry is one request or two, and whether api_error events are folded in, has to be fixed before grouping, or two measurements taken a week apart will not reconcile.
3. How to measure
The Claude Code monitoring documentation describes enabling OpenTelemetry with CLAUDE_CODE_ENABLE_TELEMETRY=1 and choosing exporters with OTEL_LOGS_EXPORTER and OTEL_METRICS_EXPORTER. Events are named claude_code.<event>; the relevant one here is api_request, whose documented attributes include model, cost_usd, cost_usd_micros, duration_ms, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, request_id, speed, effort and query_source, for which the documentation gives the examples "repl_main_thread", "compact" or a subagent name. There are aggregate metrics too: claude_code.token.usage carries a type attribute (input, output, cacheRead, cacheCreation) and model, and claude_code.cost.usage carries model; both also carry query_source, but the currently documented metric categories are too broad to identify summary calls directly. This post uses individual events for attribution; check the category granularity before using metrics.
The documentation defines input_tokens as excluding cache reads and cache creation. Adding all four fields gives the total input and output token count for the request; it includes output and should not be described as the amount sent to the model. Counting input_tokens alone omits cached input.
Without a query_source field, on an older version or a different tool, you can look for candidates by shape, a fixed interval and a prompt that keeps growing. That is a lead, nothing more. The gist author only became certain after grouping by query_source. A set of requests picked out by pattern has unknown contents, and any cost computed from it should be labelled as suspected. Requests you cannot classify go on a separate “unknown” line rather than being forced into a category.
What follows is a demonstration on synthetic data. The field names follow the documented api_request event, but the record format is simplified and the values and rates are made up. A real telemetry export cannot be fed directly into this example. The script produces seven days of records: a main thread active four hours a day at roughly one request a minute; zero to six background agents a day, each running an hour at roughly one request every 45 seconds; and for each agent one agent_summary row every 30 seconds with tiny input and output and a cache_read_tokens value that grows with the transcript. The seed is fixed at 42.
# Synthetic api_request records (field names follow the Claude Code OTel docs), written as JSONL
import json, random
random.seed(42)
RATE = {"in": 5.0, "out": 25.0, "cr": 0.5, "cc": 6.25} # USD per 1M tokens, made-up rates, not list price
DAY = 86400
PLAN = [3, 1, 1, 0, 2, 6, 5] # seven days; background agents started per day (1 hour each)
def req(t, source, inp, out, cr, cc):
cost = (inp*RATE["in"] + out*RATE["out"] + cr*RATE["cr"] + cc*RATE["cc"]) / 1e6
return {"event.name": "api_request", "event.timestamp": t, "model": "synthetic-model",
"query_source": source, "input_tokens": inp, "output_tokens": out,
"cache_read_tokens": cr, "cache_creation_tokens": cc, "cost_usd": round(cost, 6)}
rows = []
for day, n_agents in enumerate(PLAN):
t0 = day * DAY + 9 * 3600
for k in range(240): # main thread: roughly every 60 s for 4 hours
rows.append(req(t0 + k*60 + random.randint(0, 20), "repl_main_thread",
random.randint(500, 3000), random.randint(100, 800),
100000 + 300*k, random.randint(0, 4000)))
for a in range(n_agents):
ta = t0 + 600 + a * 300
ctx = 20000
for k in range(80): # the agent itself: roughly every 45 s for 1 hour
ctx += random.randint(1500, 4000)
rows.append(req(ta + k*45, f"agent-{a}", random.randint(300, 2000),
random.randint(100, 600), ctx, random.randint(0, 3000)))
for k in range(120): # status summary: every 30 s, reads the whole transcript
rows.append(req(ta + k*30 + 5, "agent_summary", 60, random.randint(5, 20),
15000 + k * 1000, 0))
rows.sort(key=lambda r: r["event.timestamp"])
with open("requests.jsonl", "w") as f:
for r in rows:
f.write(json.dumps(r) + "\n")
print(len(rows), "requests written")
5280 requests written
Next, group by query_source, folding the agents’ own requests (agent-0, agent-1 and so on) into one class, and sum request counts, token kinds and estimated cost. The last block prints the three shares side by side:
# Group by query_source: requests, token kinds, estimated cost, and three shares side by side
import json, collections
agg = collections.defaultdict(lambda: collections.Counter())
for line in open("requests.jsonl"):
r = json.loads(line)
src = "subagent" if r["query_source"].startswith("agent-") else r["query_source"]
c = agg[src]
c["requests"] += 1
c["input"] += r["input_tokens"]; c["output"] += r["output_tokens"]
c["cache_read"] += r["cache_read_tokens"]; c["cache_creation"] += r["cache_creation_tokens"]
c["cost_usd"] += r["cost_usd"]
total = collections.Counter()
for c in agg.values():
total.update(c)
total["tokens"] = total["input"] + total["output"] + total["cache_read"] + total["cache_creation"]
print(f"{'query_source':17}{'requests':>9}{'input':>10}{'output':>9}{'cache_read':>13}{'cost_usd':>10}")
for src, c in sorted(agg.items(), key=lambda kv: -kv[1]["cost_usd"]):
print(f"{src:17}{c['requests']:9}{c['input']:10}{c['output']:9}{c['cache_read']:13}{c['cost_usd']:10.2f}")
print()
for src, c in agg.items():
tokens = c["input"] + c["output"] + c["cache_read"] + c["cache_creation"]
print(f"{src:17} requests {c['requests']/total['requests']:6.1%} "
f"tokens {tokens/total['tokens']:6.1%} cost_usd {c['cost_usd']/total['cost_usd']:6.1%}")
query_source requests input output cache_read cost_usd
repl_main_thread 1680 2973883 748500 228228000 168.62
subagent 1440 1629914 506539 188531520 129.02
agent_summary 2160 129600 26923 160920000 81.78
repl_main_thread requests 31.8% tokens 39.9% cost_usd 44.4%
subagent requests 27.3% tokens 32.7% cost_usd 34.0%
agent_summary requests 40.9% tokens 27.3% cost_usd 21.6%
The same request class accounts for 40.9%, 27.3% and 21.6%, depending on the denominator. In this synthetic dataset, summaries are frequent but carry fewer tokens per call, mostly cache reads, so their cost share is lower still. The figures illustrate the calculation and do not predict the overhead in another workflow.
4. A share with a different denominator is not comparable
The gist compares a week with two days and reports a summary share of 0% after interception. That supports the observation that no summary calls were recorded in that period; it does not establish a 16.4% reduction in total spending. The comparability of the workloads, agent use and cache behaviour needs verification.
Here is the same synthetic log cut three ways:
# Same log, different measurement windows: the agent_summary share moves a lot
import json, collections
DAY = 86400
rows = [json.loads(l) for l in open("requests.jsonl")]
def share(lo_day, hi_day):
win = [r for r in rows if lo_day*DAY <= r["event.timestamp"] < hi_day*DAY]
cost = sum(r["cost_usd"] for r in win)
summ = sum(r["cost_usd"] for r in win if r["query_source"] == "agent_summary")
agent_hours = sum(1 for r in win if r["query_source"].startswith("agent-")) * 45 / 3600
return cost, summ, agent_hours
for name, lo, hi in [("days 1-7", 0, 7), ("days 3-4", 2, 4), ("days 6-7", 5, 7)]:
cost, summ, ah = share(lo, hi)
per_day = cost / (hi - lo)
per_ah = summ / ah if ah else float("nan")
print(f"{name:9} total {cost:7.2f} per day {per_day:6.2f} summary share {summ/cost:6.1%}"
f" agent-hours {ah:5.1f} summary cost per agent-hour {per_ah:5.2f}")
days 1-7 total 379.43 per day 54.20 summary share 21.6% agent-hours 18.0 summary cost per agent-hour 4.54
days 3-4 total 59.55 per day 29.77 summary share 7.6% agent-hours 1.0 summary cost per agent-hour 4.54
days 6-7 total 176.83 per day 88.41 summary share 28.3% agent-hours 11.0 summary cost per agent-hour 4.54
In this synthetic log, each agent produces a summary every 30 seconds. Yet days 3 to 4 give a share of 7.6%, days 6 to 7 give 28.3%, and the week gives 21.6%, because the amount of agent work varies. Even if summaries were disabled on day 3, comparing total spending on days 6 to 7 with the earlier period would not isolate the effect: the workloads were different already.
The right-hand column divides summary cost by background agent running hours. It is about 4.54 in each window because every synthetic agent follows the same summary frequency and token model. Whether that rate is useful in a real workflow needs verification; someone else’s 16.4% is no substitute for measuring it. Record concurrent agents and their running times, distinguish those from the total number created, and check when the tool’s timers actually run.
5. What to compare before switching it off
Even with a measurement in hand, a few things come before the decision:
- Run several times under fixed conditions. Same task, same model, same tool version, a few rounds each way. A single rerun is dominated by prompt cache hit rate and differences in execution path.
- Record completion quality and wall-clock time alongside cost. Whether summaries serve only the status display needs verification. They may leave the result unchanged if no other workflow depends on them, but the gist’s method alters a timer and its side effects need checking.
- Find every consumer of the status first. The terminal panel is one; the SDK’s
task_progressevent is another, and an automation that depends on it may fail when the summaries stop. - Do not infer that a whole file was sent because an agent read it. A summary request sends the transcript; start by checking the
api_request_bodyevent withOTEL_LOG_RAW_API_BODIESenabled, as the issue author reports doing. The export can itself be truncated or redacted, so it cannot always establish the full payload.
The destination also needs checking against the actual configuration. The public report describes summaries going to the model API used by the agent. Even if the provider is unchanged, that alone does not settle the data-handling implications; the transcript sent, any truncation and the receiving endpoint still need verification.
Notes
- The public measurement is by a single author whose interception tests cover versions 2.1.257 to 2.1.312:
agent_summaryat 16.4% of estimated cost and 19.7% of requests, with cost being the tool’s owncost_usdestimate rather than a bill. - The 18% to 26% in the GitHub issue are a weighted-token share and a cache-read-token share from different versions and session lengths, not one quantity.
- Attribute by
claude_code.api_requestevent andquery_source; keep request count, tokens, estimated cost and the bill as four separate figures. - In the synthetic data the same request class shows 40.9%, 27.3% and 21.6% for request, token and cost share, because it fires often and its tokens are mostly cheap cache reads.
- Changing the measurement window moves the share from 7.6% to 28.3%; summary cost per agent-hour is approximately fixed in this synthetic model, and its use elsewhere needs verification.
- Before switching it off, rerun under fixed conditions, record quality and time, and check whether anything consumes the SDK’s
task_progressevents.
Further reading
- flamerix-ai, Claude Code burns ~15–20% of your tokens on a 3-word status line. Here’s how to turn it off: the source for that author’s measurements, tested version range, measurement periods and Bun preload method; the issue reports and this post’s synthetic data are separate.
- Claude Code documentation, Monitoring usage: how to enable OpenTelemetry, the attribute names on the
api_requestevent, and thetoken.usageandcost.usagemetrics; the field names here follow it. - GitHub issue, Feature request: a supported way to disable the agent_summary spinner: a second measurement, one four-hour session at 22.7% of requests and 18.0% of weighted tokens, with a comment reporting 26% of cache-read tokens.
- Hacker News, Claude Code regularly sends each background agent’s transcript to its model: other users’ reports and the prompt cache discussion; no billing-level confirmation.
- This site, Estimating AI Prototypes in Two Stages: From Zero to Demo, From Demo to Production: cited for estimating total AI workflow cost; the page could not be retrieved and its contents need verification.
- This site, Counting the Cost of AI Code Review: Bugs Found, False Alarms and People’s Time: cited for the cost of multi-agent review; the page could not be retrieved and its contents need verification.
Ideas and technical judgement by Sheng; drafted with Claude · topic chosen by Sheng; text, case notes and examples written by Claude, run on Linux with Python 3.13.18.