---
title: Which Claude compaction mode should you use, threshold or on-demand?
url: https://deepthinkingai.org/claude-compaction-modes/
published: 2026-09-16
author: Shekhar Singh
topic: AI Engineering
tags: context-management, compaction, anthropic-api, agents, cost
site: DeepThinking AI
---

# Which Claude compaction mode should you use, threshold or on-demand?

**Summary:** Claude now has two compaction features with the same name. Threshold compaction, beta compact_20260112, leaves its summary block after the messages it summarises. On-demand compaction, beta compact-2026-09-04, returns a signed block that replaces them and must be sent first, or the request returns a 400.

## 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](https://platform.claude.com/docs/en/build-with-claude/compaction)
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**

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 --- (Summary follows the messages it covers)
3. Turns 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**

1. Signed compaction block, first in messages
   Carries a signature you must not alter
--- summarised messages must be gone --- (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

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](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons)
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**

```mermaid
sequenceDiagram
    participant Yourapp as Your app
    participant MessagesAPI as Messages API
    Yourapp->>MessagesAPI: messages + compaction: summarize
    MessagesAPI-->>Yourapp: compaction block, stop_reason compaction
    Yourapp->>Yourapp: Swap the summarised range for the block
    Yourapp->>MessagesAPI: block first, then the kept turns
```

- messages + compaction: summarize: Same system and tools as the rest of the conversation.
- compaction block, stop_reason compaction: One block, a signature, and no reply text.
- Swap the summarised range for the block: Drop exactly the messages you sent, keep the rest.

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](/prompt-caching-economics/) 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.

<ReadNext
  href="/long-context-vs-retrieval/"
  kicker="Related"
  title="When filling the context window is actually the right call"
  note="Compaction is one answer to a long history. Retrieval is the other, and they fail differently."
/>

## 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](/long-context-vs-retrieval/) 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.

## 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
- [Compaction](https://platform.claude.com/docs/en/build-with-claude/compaction). Anthropic
- [Claude API release notes](https://platform.claude.com/docs/en/release-notes/overview). Anthropic
- [Handling stop reasons](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons). Anthropic
- [Preserved thinking](https://platform.claude.com/docs/en/build-with-claude/preserved-thinking). Anthropic

---
Canonical HTML: https://deepthinkingai.org/claude-compaction-modes/