Which Claude compaction mode should you use, threshold or on-demand?
AI Architect
Key takeaways
- The two compaction betas put the summary block on opposite sides of the messages it covers.
- A signed block sent with summarised messages still in front of it returns a 400 compaction_block_misplaced.
- A compaction request that produces no summary returns HTTP 200 with empty content and is still billed.
- Top-level input_tokens and output_tokens are zero on a compaction request, so the cost sits only in usage.iterations.
- Images, documents and fetched URLs inside the summarised range are gone once the block replaces them.
Anthropic shipped a second compaction mechanism on 14 September 2026. It shares a name, a block type and a mental model with the one already in the API, and it reverses the rule about where the summary goes. A team migrating between the two by pattern-matching on the old code will hit a 400 with no obvious cause.
What does Claude’s compaction parameter actually do?
Compaction replaces a stretch of conversation with a model-written summary of it, so a long-running agent stops paying for history it no longer needs in full.
Two separate features now do this. Threshold compaction is configured through
context_management.edits with a compact_20260112 entry and a token trigger,
and the API fires it partway through a request once input tokens cross that
trigger. On-demand compaction is the compact-2026-09-04 beta. You send a
top-level compaction parameter on a request of your choosing, and the
documentation
describes the result plainly: “The response contains a single signed compaction
block and no reply.”
The second mechanism exists because the first one takes the decision away from you. Threshold compaction pauses an ordinary request to write a summary. On-demand compaction lets that summarisation run as its own request, in the background, on a schedule your application picks.
How do threshold and on-demand compaction place the block differently?
This is the part that breaks a migration. The two mechanisms put the block on opposite sides of the text it summarises.
Where threshold compaction leaves the block
Show as text
| # | Layer | Note |
|---|---|---|
| 1 | System prompt and tool definitions | Unchanged by compaction |
| 2 | Older messages, still present in your array | The API ignores everything before the block |
| · | compaction block appears here (breakpoint) | Summary follows the messages it covers |
| 3 | Turns taken after the trigger fired |
Under threshold compaction the block follows the messages it covers, and everything before it is ignored, so you may leave the old messages in your array and let the API skip them.
Where an on-demand signed block must sit
Show as text
| # | Layer | Note |
|---|---|---|
| 1 | Signed compaction block, first in messages | Carries a signature you must not alter |
| · | summarised messages must be gone (breakpoint) | Any left in front returns a 400 error |
| 2 | Turns kept word for word after the summary | Thinking here can stay valid on some models |
| 3 | The new user turn |
A signed block replaces them instead. The docs are explicit: “Leaving the
summarized messages in front of a signed block is a 400 error”, returned with
error.details.error_code set to compaction_block_misplaced. Messages left
after the block are not rejected, and get sent to the model a second time,
which is the quieter version of the same mistake. Send the block first, exactly
as returned, signature included, on every subsequent request.
Why does a failed compaction return HTTP 200?
Because the summarisation call is a normal model call, and a model call can end without producing usable text while still succeeding at the HTTP level.
A summary comes back only when that call ends with text and no tool use.
Otherwise the response is a 200 with empty content, and stop_reason carries
the reason:
stop_reason |
What happened | What to change |
|---|---|---|
max_tokens |
The summary was cut off | Raise max_tokens and resend |
model_context_window_exceeded |
No room for the summarisation prompt | Send fewer messages |
refusal |
Safeguards declined the request | Read stop_details for the category |
tool_use |
The model called a tool instead | Tell it not to, in instructions |
end_turn |
The call returned no text | Resend |
Retry logic that branches on status codes will read every one of these as
success and then swap in a block that does not exist. Branch on stop_reason
instead, the same discipline the
stop reason documentation
asks for elsewhere. The call is billed either way, and reported in
usage.iterations, so a run that silently fails to compact still shows up on
the invoice as though it worked.
One case does use a status code. A transient server problem while producing or
reading a block returns a retryable 529 with error.details.error_code set to
compaction_unavailable. Everything else specific to this beta is a 400, and
most of those messages say what to remove or resend.
How does compaction change your prompt cache and your token bill?
Both numbers move, and the accounting is easy to read wrong.
Asking for a summary while the conversation keeps running
Show as text
| # | From | To | Message |
|---|---|---|---|
| 1 | Your app | Messages API | messages + compaction: summarize. Same system and tools as the rest of the conversation. |
| 2 | Messages API | Your app | compaction block, stop_reason compaction. One block, a signature, and no reply text. |
| 3 | Your app | Your app | Swap the summarised range for the block. Drop exactly the messages you sent, keep the rest. |
| 4 | Your app | Messages API | block first, then the kept turns |
On a compaction request the top-level input_tokens and output_tokens are
zero, because no reply was generated. The real usage sits in usage.iterations
as a compaction entry. A cost dashboard that sums the top-level fields, which
is what most of them do, will record every compaction as free and then show an
unexplained gap against the invoice.
The cache interaction is more useful. Swapping a block in rewrites the front of
the prompt, so the conversation prefix is invalidated by definition. What you can
protect is the part above it: put a cache_control breakpoint at the end of the
system prompt and the system prompt stays cached across the swap, leaving only
the summary to be written fresh. This is the same ordering discipline that
prompt caching rewards everywhere else, applied at
a point where the prefix is guaranteed to change.
What does compaction silently drop from the conversation?
More than the token count suggests, and the losses are not symmetrical.
Images, documents, container_upload blocks and fetched URLs inside the
summarised range stop existing once the block replaces them. The summary is text.
Anything a later turn still needs has to be restated or re-uploaded, and nothing
in the response tells you that a picture just left the conversation.
Mid-conversation system messages inside the range are summarised too, and what
they declared stops applying. If an instruction still matters, send it again
after your next new user turn rather than wedging it between the block and the
kept turns, because that position breaks the kept turns’ thinking.
Thinking blocks survive only under two conditions together: the kept turns
directly followed the summarised messages, and system plus the tools not marked
defer_loading are unchanged from the compaction request. Change a tool
definition and compact in the same step and the thinking is gone.
Which compaction mode should you use?
For anything that looks like a chat product, threshold compaction. You set a trigger, the API handles the rest, and the block placement rule is forgiving. The cost of the managed version is a pause inside a user-facing request, which a chat turn can absorb.
For a long-running agent, on-demand. Three things make the difference, and only the third is about the context window itself. You choose the moment, so compaction never lands mid-task. The summarisation runs as its own request while the agent keeps working on full history. And you can keep a tail of recent turns word for word by simply leaving them out of the compaction request, which on models with preserved thinking keeps the agent’s train of thought intact across the swap.
The honest caveat is that on-demand compaction is Claude API only and in beta, while threshold compaction is already available on Bedrock, Vertex and Foundry. If you are multi-cloud, that decides it for you before any of the above does.
What is still unclear?
One thing the documentation does not settle: how much a summary costs relative to what it saves, over a realistic agent run.
The summarisation call is billed at the full input price of the conversation it reads, and a second compaction later summarises the first summary plus everything after it. Nothing published states how that series behaves over a long task, and the answer depends on your summary length and how often you compact, neither of which the docs suggest a value for. The 50,000-token minimum trigger on threshold compaction is the only hint of an intended floor.
Measure it before assuming compaction is cheaper than a larger context window.
Log each compaction entry in usage.iterations against the tokens the next
request would have carried without it. That comparison is a per-application
number, and it is the one that decides whether this feature earns its complexity
for your workload.
Do this
Move a long-running agent onto on-demand compaction
The swap is small. The failure modes are all about ordering and about a failed summary that arrives looking like a success.
Send the beta header on every request in the conversation
compact-2026-09-04 is needed on the request that asks for the summary and on every later request carrying the block. Leaving it off produces a validation error that never mentions the header.
Compact exactly the messages of a request you already made
Send that message list with compaction set to summarize. Keeping the boundary identical to a real request is what lets the turns you keep afterwards hold their thinking.
Check stop_reason before you trust the response
A summary exists only when the call ended with text and no tool call. Otherwise you get a 200 with empty content and a stop_reason naming the cause: max_tokens, model_context_window_exceeded, refusal, tool_use or end_turn.
Drop exactly the messages you sent, then put the block in front
Remove the summarised range from the head of your history and prepend the returned assistant message. Any summarised message left in front of the block returns a 400.
Re-upload anything the summary cannot carry
Images, documents, container uploads and fetched URLs inside the summarised range stop existing for the model. Restate or re-send whatever a later turn still needs.
Put a cache breakpoint at the end of the system prompt
That keeps the system prompt cached separately, so a compaction only invalidates the conversation portion of the prefix instead of the whole thing.
Sum usage.iterations rather than the top-level token counts
The top-level fields report zero on a compaction request. A cost dashboard that reads them will record compaction as free.
Frequently asked questions
- Can I send both compaction parameters on one request?
- No. The API rejects a request that carries both the top-level compaction parameter and context_management. Threshold compaction also cannot run on a request that already carries a signed block, so a conversation uses one mechanism at a time.
- What happens if I forget the beta header?
- The request fails with a generic validation error such as "compaction: Extra inputs are not permitted", which does not mention the header. The header is needed both on the request that asks for the summary and on every later request that carries the block.
- Does compaction keep the model's earlier thinking?
- Only for turns kept after the summary, and only on models with preserved thinking. The kept turns must have directly followed the summarised messages, and system plus non-deferred tools must be unchanged from the compaction request.
- Is the summarisation call billed?
- Yes. It is billed and rate limited like any other request and reported as the compaction entry in usage.iterations, including when it returns no summary at all.
- How many compaction blocks can one request carry?
- Exactly one, on every later request. A request without the block reaches the model with no summary, and a duplicated block is rejected.