
Claude's Compaction API: Long Sessions Without Context Rot
Compaction is a beta feature of the Claude Messages API that summarizes earlier conversation history server-side once a session crosses a token threshold you set. Turn it on with the beta header compact-2026-01-12 and a context_management edit of type compact_20260112. The summary comes back as a compaction content block, and every later request has to carry it back — because the API ignores every block that came before it. Get that one detail wrong and the feature silently does nothing while you pay for it twice.
What Compaction Actually Does
A long agent run doesn't fail the moment the context window fills. It gets worse gradually, well before that — instructions from turn three stop landing, the model re-litigates decisions it already made, and the useful signal drowns in three hundred tool results nobody needs anymore.
Compaction is Anthropic's server-side answer. When the request's input tokens cross your trigger value, the API runs an extra pass that summarizes the conversation so far, then continues with that summary in place of the history. You get back a content block that looks like this:
{
"type": "compaction",
"content": "[summary text]"
}The important part isn't the block, it's the rule attached to it: the API automatically ignores every content block prior to the compaction block on subsequent requests. You keep appending to the same messages array as always. The server just stops reading the part above the waterline.
The One Line That Silently Breaks It
Here is the bug you will write, because everyone writes it once. Most chat loops accumulate history like this:
# Wrong — discards the compaction block
text = response.content[0].text
messages.append({"role": "assistant", "content": text})That works fine for a normal turn and destroys a compacted one. The summary lives in the compaction block, not the textblock. Pull out the string and you've thrown the summary away — so the next request ships the entire uncompacted history again, blows through the trigger again, and compacts again. You pay the compaction pass on every single turn and never once benefit from it. Nothing errors. Your logs look normal.
# Right — the whole content array
messages.append({"role": "assistant", "content": response.content})Append the array. Every time. If you have a helper that normalizes assistant turns into strings somewhere in your stack, that helper is now a bug.
The Config, and What Each Knob Costs You
The whole surface is four fields. Two of them matter more than the docs let on.
| Field | Default | What to know |
|---|---|---|
| type | required | Must be compact_20260112. Not clear_tool_uses_20250919— that's a different feature entirely. |
| trigger.value | 150,000 | Minimum 50,000. Only {"type": "input_tokens"} is supported. Set it low and you compact constantly; set it near your window and you get no headroom for the answer. |
| instructions | null | Replaces the default prompt completely — it does not append to it. Write a full summarization brief or leave it alone. |
| pause_after_compaction | false | Set true and you get stop_reason: "compaction" so you can inspect or log the summary before continuing. |
The instructions footgun deserves emphasis. It reads like a nudge — “keep the customer's account ID” — and it is actually a full replacement of Anthropic's tuned default prompt. Drop a one-liner in there and you have quietly swapped a well-engineered summarizer for your one-liner. If you need to prioritize specific state, spell out everything you want kept, not just the new thing.
Use pause_after_compaction in development:
Turn it on, read the summaries your real traffic produces, and check whether the thing your agent needs on turn 40 actually survived. That is the only honest way to decide whether you need custom instructions. Then turn it off in production.
The Billing Trap Nobody Warns You About
This is the part that will actually cost you money, and it's one sentence in the docs. Top-level usage.input_tokens and usage.output_tokens do not include the compaction iteration. A compacting turn runs two passes, and the top-level fields report only the second one.
The real numbers live in usage.iterations, an array with one entry per pass, each tagged compaction or message. Take the shape straight from Anthropic's documentation — a compaction pass of 180,000 in / 3,500 out, then a message pass of 23,000 in / 1,000 out — and run the arithmetic:
| What you log | Input | Output | Cost at Opus 5 rates |
|---|---|---|---|
Top-level usage | 23,000 | 1,000 | ~$0.14 |
Summed usage.iterations | 203,000 | 4,500 | ~$1.13 |
Same turn. Roughly 8x the cost you recorded. If your dashboard reads top-level usage — and most do, because that's what every pre-compaction example used — your spend chart is fiction on exactly the turns that matter most. I flagged something similar when digging into Claude API rate limits: the headers and usage fields carry more than the obvious reads, and the obvious read is usually the wrong one.
Fix it in one place, at the seam where you record usage:
iters = response.usage.iterations or []
total_in = sum(i.input_tokens for i in iters) or response.usage.input_tokens
total_out = sum(i.output_tokens for i in iters) or response.usage.output_tokensThe fallback matters: on a turn where compaction didn't fire, treat the top-level fields as the source of truth.
Compaction vs Context Editing: Pick the Right One
These two get conflated constantly, and it's understandable — they share the context_management parameter. They are separate features with separate beta headers, and mixing the values up gets you a 400.
| Compaction | Context editing | |
|---|---|---|
| Does what | Summarizes old history | Deletes old blocks |
| Beta header | compact-2026-01-12 | context-management-2025-06-27 |
| Edit type | compact_20260112 | clear_tool_uses_20250919 clear_thinking_20251015 |
| Extra token cost | Yes — a full summarization pass | No — it just drops blocks |
| Reach for it when | The conversation itself carries meaning you need later | The bulk is stale tool output nobody will read again |
The practical heuristic: if a tool-heavy agent is drowning in 200 old file reads, context editing (docs) is cheaper and does the job — you're not summarizing garbage, you're deleting it. If a long support or research thread has genuine narrative you'd lose, compaction is worth the extra pass. Plenty of production agents run both: clear the dead tool results continuously, compact the surviving conversation when it gets long.
Keep Your Prompt Cache Alive Through a Compaction
Compaction rewrites the middle of your prompt, which is exactly the sort of thing that nukes a prompt cache if you're careless. The fix is to cache the stable part on its own breakpoint so it survives the event:
system=[{
"type": "text",
"text": SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral"}, # cached separately
}]The compaction block takes cache_control too, so after the event only the summary has to be written to cache rather than the whole reconstructed prefix. This stacks with everything in prompt caching — and since cached input tokens don't count against your input-tokens-per-minute ceiling, the two features compound nicely on long agent runs.
Two smaller behaviors worth knowing before you ship:
- ▸Streaming: a compaction block arrives as one
content_block_start, onecontent_block_deltaof typecompaction_deltacarrying the complete summary, thencontent_block_stop. It does not stream incrementally, so don't build a progress UI expecting it to. - ▸Token counting:
/v1/messages/count_tokensapplies existing compaction blocks but never triggers a new compaction. It also returnscontext_management.original_input_tokens, so you can see the pre-compaction size — useful for deciding whether your trigger is set sensibly.
When Not to Turn This On
Compaction is a good default for long-running conversational agents. It is a bad default for everything else, and the beta header being cheap to add makes it tempting to add everywhere.
Skip it when your sessions never approach 50,000 input tokens — the trigger can't go lower, so you've added a beta dependency that will never fire. Skip it on anything where losing verbatim detail is unacceptable; a summary is lossy by construction, and “the model summarized away the exact error string” is a miserable bug to chase. And skip it if your real problem is architectural — an agent that fills a million tokens of context on one task usually needs subagents with their own fresh windows, not a better summarizer bolted onto one enormous thread.
Where it genuinely earns its place: support threads that run for hours, research agents that read a lot and reason across all of it, and anything with a human in the loop coming back the next day. For those, compaction plus real cost controls is the difference between an agent that degrades at turn 30 and one that doesn't.
One last operational note: it's beta on every platform that has it — the Claude API, Claude Platform on AWS, Amazon Bedrock, Google Cloud and Microsoft Foundry. Beta headers change. Pin it, log it, and put a note where your team will find it when a request starts returning 400.
Agent Degrading Halfway Through Long Runs?
I build and audit production Claude agents — context strategy that picks compaction, clearing or subagents on purpose, caching laid out so it actually hits, and token accounting that reports what you really spent instead of what the top-level field says. If your long sessions get vague around turn 30, that's a fixable problem.
Related Posts
AI Models
Claude API 429 Rate Limits: How to Fix Them in Production
A Claude API 429 isn't one limit, it's three — RPM, ITPM and OTPM — enforced per model at the organization level on a token bucket that refills continuously. Honor retry-after before you reach for backoff, add jitter so your workers stop stampeding, log the anthropic-ratelimit headers so you throttle before the error fires, and share one limiter across every worker. Plus the fix almost nobody mentions: cached input tokens don't count toward ITPM, so prompt caching raises your effective ceiling roughly 5x at an 80% hit rate.
AI Models
Claude Sonnet 5 vs Opus 4.8 (2026): The Cost Math I Actually Use to Pick
Sonnet 5 is cheaper per token; Opus 4.8 usually finishes hard, open-ended work in fewer tokens and fewer retries — so the number that decides it is cost-per-completed-task, not cost-per-million. The at-a-glance table (pricing + agentic-coding benchmarks), the loop signal I use to escalate, and real numbers from the Claude Code agents I run in production: Sonnet 5 by default, Opus 4.8 for the hard tail.
AI Models
ComfyUI Best Practices: My Production Image Pipeline on an RTX 5090
Hard-won ComfyUI best practices from a production SDXL pipeline — native resolution vs upscaling, FaceDetailer for tack-sharp eyes, LoRA OOM fixes, and reusable workflow architecture.