DeepThinking AI

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

Where threshold compaction leaves the blockDiagram: 4 ordered layers. System prompt and tool definitions, then Older messages, still present in your array, then compaction block appears here (breakpoint), then Turns taken after the trigger fired.1System prompt and tool definitionsUnchanged by compaction2Older messages, still present in your arrayThe API ignores everything before the blockcompaction block appears here3Turns taken after the trigger fired
Show as text
Where threshold compaction leaves the block. Diagram: 4 ordered layers. System prompt and tool definitions, then Older messages, still present in your array, then compaction block appears here (breakpoint), then Turns taken after the trigger fired.
#LayerNote
1System prompt and tool definitionsUnchanged by compaction
2Older messages, still present in your arrayThe API ignores everything before the block
·compaction block appears here (breakpoint)Summary follows the messages it covers
3Turns taken after the trigger fired
Threshold compaction fires partway through a request once input tokens cross your trigger. You may leave the summarised messages in place, because the block sits behind them and the API ignores what precedes it.

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

Where an on-demand signed block must sitDiagram: 4 ordered layers. Signed compaction block, first in messages, then summarised messages must be gone (breakpoint), then Turns kept word for word after the summary, then The new user turn.1Signed compaction block, first in messagesCarries a signature you must not altersummarised messages must be gone2Turns kept word for word after the summaryThinking here can stay valid on some models3The new user turn
Show as text
Where an on-demand signed block must sit. Diagram: 4 ordered layers. Signed compaction block, first in messages, then summarised messages must be gone (breakpoint), then Turns kept word for word after the summary, then The new user turn.
#LayerNote
1Signed compaction block, first in messagesCarries a signature you must not alter
·summarised messages must be gone (breakpoint)Any left in front returns a 400 error
2Turns kept word for word after the summaryThinking here can stay valid on some models
3The new user turn
On-demand compaction inverts the rule. The block replaces the messages rather than following them, and the summarised range has to be deleted from your history before the next request goes out.

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

Asking for a summary while the conversation keeps runningSequence diagram between Your app and Messages API. 1. Your app to Messages API: messages + compaction: summarize. 2. Messages API to Your app: compaction block, stop_reason compaction. 3. Your app to Your app: Swap the summarised range for the block. 4. Your app to Messages API: block first, then the kept turns.Your appMessages APImessages + compaction: summarizeSame system and tools as the rest of the conversation.compaction block, stop_reason compactionOne block, a signature, and no reply text.Swap the summarised range for the blockDrop exactly the messages you sent, keep the rest.block first, then the kept turns
Show as text
Asking for a summary while the conversation keeps running. Sequence diagram between Your app and Messages API. 1. Your app to Messages API: messages + compaction: summarize. 2. Messages API to Your app: compaction block, stop_reason compaction. 3. Your app to Your app: Swap the summarised range for the block. 4. Your app to Messages API: block first, then the kept turns.
#FromToMessage
1Your appMessages APImessages + compaction: summarize. Same system and tools as the rest of the conversation.
2Messages APIYour appcompaction block, stop_reason compaction. One block, a signature, and no reply text.
3Your appYour appSwap the summarised range for the block. Drop exactly the messages you sent, keep the rest.
4Your appMessages APIblock first, then the kept turns
The summarisation request is separate from the conversation, so an agent can keep working on its full history until the block arrives. The swap has to happen on the first request afterwards for kept thinking to survive.

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

Sources

  1. CompactionAnthropic
  2. Claude API release notesAnthropic
  3. Handling stop reasonsAnthropic
  4. Preserved thinkingAnthropic

context-managementcompactionanthropic-apiagentscost