Claude API Message Threads (Beta): Faster Agent Loops
Message threads (beta) keep a Claude API conversation on the server, so each turn sends only new messages. How to use them, expiry, errors, and caching.
Message threads, announced on November 2, 2026, are a beta feature of the Claude Messages API. A thread keeps a conversation on the server for a short time, so after the first request each turn sends only the new messages and the previous response’s id. Per the docs, “requests are smaller, and time to first token stays lower and steadier as the conversation grows.”
Beta: every request of a thread needs the
message-threads-2026-08-12beta header (the SDK thread helper adds it for you). Threads are not ZDR eligible.
Why re-sending the conversation gets slow
In an agent loop, every tool result goes back to Claude in a request carrying the entire history. “Without a thread, every turn uploads the whole conversation again, and the API validates and tokenizes all of it again,” the docs explain. With a thread, “most of that work covers only the new turn.”
Anthropic reports that with message threads on by default for most Claude Code users, “requests with 500k to 1M tokens see median TTFT drop by about 35%,” and that “the more context an agent carries, the more message threads can reduce time to first token.” Its examples: long-running coding agents, computer use agents, and document- and image-heavy workflows.
What the server keeps, and what it doesn’t
A thread holds the conversation the API sends to the model, the system and tools from the create request, and a fingerprint of the fixed fields for checking later requests. It holds files by reference, so keep them available. MCP server URLs, MCP authorization tokens, and the container id aren’t stored; send mcp_servers and container on every request that uses them.
No API lists, reads, or deletes a thread, and no response says whether the API stored a turn. Your application “stays the source of truth for the conversation,” so keep your own copy and build the replay path from the start. (The session threads of Claude Managed Agents are a different feature.)
How to start and continue a thread
Add one parameter, thread, to a Messages API request; the response is the usual message.
- Create: send the full conversation,
system, andtools, plus"thread": {"type": "create"}. - Continue: send only the new messages, plus
"thread": {"type": "continue", "previous_message_id": "<previous id>"}, withoutsystemandtools. Use each new responseidfor the next turn. - Replay: if a continue request returns a 404 error, send the full conversation in a new create request and continue from its
id.
With the SDK thread helper
The helper takes the same fields as a Messages request, adds the beta header, and keeps the full conversation in memory. Python and TypeScript (the docs cover seven languages):
client = anthropic.Anthropic()
# Create the helper. This call sends no request.
thread = client.beta.messages.thread(
model="claude-opus-5-5",
max_tokens=1024,
cache_control={"type": "ephemeral"},
system="You are a support agent for Acme. Keep replies short.",
)
# Turn 1: the helper creates a thread and sends the full conversation
thread.send({"role": "user", "content": "Hi! I have a question about an order."})
# Turn 2: the helper continues the thread and sends only the new message
second = thread.send(
{"role": "user", "content": "How long does standard shipping usually take?"}
)
for block in second.content:
if block.type == "text":
print(block.text)
const client = new Anthropic();
// Create the helper. This call sends no request.
const thread = client.beta.messages.thread({
model: "claude-opus-5-5",
max_tokens: 1024,
cache_control: { type: "ephemeral" },
system: "You are a support agent for Acme. Keep replies short."
});
// Turn 1: the helper creates a thread and sends the full conversation
await thread.send({ role: "user", content: "Hi! I have a question about an order." });
// Turn 2: the helper continues the thread and sends only the new message
const second = await thread.send({
role: "user",
content: "How long does standard shipping usually take?"
});
for (const block of second.content) {
if (block.type === "text") {
console.log(block.text);
}
}
The top-level cache_control is optional. The SDK’s beta tool runner (the tool_runner() loop from our MCP helpers walkthrough) also uses a thread by default; create it with thread=False in Python or thread: false in TypeScript to opt out.
Sending the thread field yourself
With cURL, the CLI, or an SDK’s create method, send the beta header on every request:
# Turn 1: send the full conversation and create a thread
curl -sS --fail-with-body https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2026-10-21" \
-H "anthropic-beta: message-threads-2026-08-12" \
-H "content-type: application/json" \
-d '{
"model": "claude-opus-5-5",
"max_tokens": 1024,
"cache_control": {"type": "ephemeral"},
"system": "You are a support agent for Acme. Keep replies short.",
"messages": [{"role": "user", "content": "Hi! I have a question about an order."}],
"thread": {"type": "create"}
}'
# Turn 2: send only the new message. Set message_id to the id of the turn 1 response.
# Leave out system and tools: the thread keeps them.
curl -sS --fail-with-body https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2026-10-21" \
-H "anthropic-beta: message-threads-2026-08-12" \
-H "content-type: application/json" \
-d '{
"model": "claude-opus-5-5",
"max_tokens": 1024,
"cache_control": {"type": "ephemeral"},
"messages": [{"role": "user", "content": "How long does standard shipping usually take?"}],
"thread": {"type": "continue", "previous_message_id": "'"$message_id"'"}
}'
Turn 2 repeats the fixed fields. In Python, pass the same thread values and betas=["message-threads-2026-08-12"] to client.beta.messages.create(...).
What can change between requests
| Group | Fields | Rule |
|---|---|---|
| Create-only | system, tools | Create request only. Sending either on a continue request, even as null, returns a 400 error. |
| Fixed | model, tool_choice, thinking, mcp_servers (apart from each server’s authorization token), context_management, output_config, the skills in container, the top-level cache_control, and the set of anthropic-beta values | Same values on every request. A different value returns a 400 error, thread_fingerprint_mismatch, naming the fields. |
| Free | max_tokens, stop_sequences, stream, metadata, service_tier, and fields the docs don’t name | Can change from turn to turn. |
To change a create-only or fixed field, start a new thread. With server-side fallback, send fallbacks on every request of a thread or on none. On Claude Fable 5.1, Mythos 5.1, Opus 5.5, Sonnet 5.5, and Haiku 5.5, changing system or tools invalidates the thinking and redacted_thinking blocks already in the conversation, and the create request can return a 400 error; leave those blocks out, and the model answers without that earlier reasoning.
For messages:
- Send only new ones. The API doesn’t check them against what it already has, so a resent message can appear twice or cause an error.
- Usually start with a
usermessage: your next turn, or thetool_resultblocks that answer atool_use. - Resume
pause_turnwith"messages": []from that response’sid. A newusermessage is rejected while the turn is paused. - Append-only, continue once. Continue from the latest response
id. To edit or branch, start a new thread.
To compact, write the summary in your code and send it in a new create request with system and tools; compaction on demand isn’t available with threads. Our context compaction tutorial shows how Claude Code handles long sessions.
Lifetime, expiry, and recovery
A thread is best-effort. It expires 5 minutes after its last request (each response the API stores restarts the 5 minutes), and the API deletes it no more than 1 hour after its last use.
When the API can’t continue a thread, the continue request returns a 404 with error.details.error_code set to thread_not_found, without saying why. Causes include expiry, a turn served but not stored (such as a response ending in a tool call cut off by max_tokens), a previous response written by a fallback model, a request from a different organization, workspace, user, or service account, and content the stored conversation can’t take, such as the first image in an image-free conversation.
To recover, send the full conversation from your copy, with system and tools and without cache_control breakpoints on earlier messages, in a new create request; then continue from the new id. Check error_code first: a request for a nonexistent model also returns a 404. If the 404 persists right after a replay, the API might be unable to store that conversation; send it without thread.
- Streaming: continue only after
message_stop, even though theidarrives inmessage_start. If a continue stream drops, send it again; onthread_already_continuedorthread_continue_in_flight, replay. “Replaying is always safe.” - Refusals: after
stop_reason: "refusal", don’t continue. Start a new thread that leaves out or rephrases the turn that led to it. - Fallbacks: the API doesn’t store a turn that a fallback model wrote. If
usage.iterationshas afallback_messageentry, send the full conversation, including that response, in a new create request.
The SDK helper replays once on thread_not_found, thread_already_continued, or thread_continue_in_flight, resumes pause_turn when you call send() with no messages, and starts over after refusals and fallback-model responses. It doesn’t retry a dropped stream by itself, and thread_fingerprint_mismatch and thread_unsupported_request errors reach your code.
Errors
These errors return before the model runs, use no tokens, and leave the thread and its expiry time unchanged.
| Status | error_code | Meaning | What to do |
|---|---|---|---|
| 404 | thread_not_found | The API can’t continue the thread. | Send the full conversation in a new create request. |
| 400 | thread_fingerprint_mismatch | A fixed field differs; the message names the fields. | Keep fixed fields the same, or start a new thread. |
| 400 | thread_already_continued | Each response id can be continued once. | Continue from the latest id; replay if your client never received that response. |
| 400 | thread_continue_in_flight | Another continue from this id is still running, usually your own earlier attempt. | Let it finish and continue from its id, or replay. |
| 400 | thread_unsupported_request | A feature or organization setting that threads don’t support; error.details.unsupported names it when present. | Remove it, or send the full conversation without thread. |
Some 400 errors carry no error_code, such as a continue request without previous_message_id or with system or tools.
Supported models and platforms
Message threads are in beta on every model the docs list: claude-fable-5-1, claude-mythos-5-1, claude-fable-5, claude-mythos-5, claude-mythos-preview, claude-opus-5-5, claude-opus-5, claude-sonnet-5-5, claude-sonnet-5, claude-haiku-5-5, claude-haiku-4-5-20251001, claude-opus-4-8, claude-opus-4-7, claude-opus-4-6, claude-sonnet-4-6, claude-opus-4-5-20251101, and claude-sonnet-4-5-20250929.
The platforms, all in beta, are the Claude API, Claude Platform on AWS, Claude Models on Google Cloud (Anthropic managed), and Microsoft Foundry. On Claude Platform on AWS, sign every request of a thread with credentials for the IAM role or IAM user that created it.
Data handling, pricing, and rate limits
Message threads are not ZDR eligible, though using them doesn’t affect your use of other ZDR-eligible features. The API “holds a thread in memory, encrypted under a server-managed key, never writes it to durable storage, and deletes it no more than 1 hour after its last use.” Threads are not HIPAA eligible either.
“Threads do not change pricing or rate limits.” As Anthropic’s announcement puts it, “Claude still reads the full conversation on every turn, so token usage stays the same.”
Message threads vs. prompt caching
Threads leave pricing unchanged, so prompt caching stays your cost lever, and “a thread does not cache your prompt for you.” On a thread:
- Send a top-level
cache_control, a fixed field, on every request; each turn then reads the earlier conversation from the cache and writes the new turn. - A request with
threadcarries at most 4cache_controlbreakpoints. The top-level one counts, the create request’ssystemandtoolsbreakpoints count again on every continue, and breakpoints on earlier stored messages don’t. One too many returnsthread_unsupported_request.
| Message threads | Prompt caching | |
|---|---|---|
| What it cuts | Upload size and per-turn validation and tokenization, for a lower time to first token | Processing time and cost, by resuming from a cached prefix |
| Pricing | Unchanged | Cache reads at 0.1× the base input price or less, depending on the model; writes at 1.25× (5-minute) or 2× (1-hour) |
| Lifetime | Expires 5 minutes after the last request | 5 minutes by default, refreshed on each use; 1-hour option at additional cost |
| ZDR | Not eligible | Eligible, excluding Covered Models |
Use both in long, active agent loops, as the threads docs’ examples do. Use caching alone for Message Batches, ZDR-bound traffic, or turns more than 5 minutes apart, where a 1-hour cache can still hit after a thread has expired.
When not to use message threads
Leave thread off when:
- Turns are usually more than 5 minutes apart, as when a human reviews each step: every continue becomes a 404 plus a replay.
- You use Message Batches or token counting, which reject
thread. - You rely on compaction or context editing (except
clear_thinking_20251015withkeep: "all"). - Your organization requires ZDR for this traffic, or has HIPAA readiness enabled or Inference hooks turned on.
- You often rewrite history or switch
system,tools, ormodel. Each change means a new thread. - Requests are one-shot. “Threads help most in long agent loops.”
Decision checklist for agent builders
Adopt threads when sessions are active and context grows large, you run on one of the four platforms, and nothing above applies. Before you ship:
- Your app keeps the full conversation and can replay it.
- Create-only and fixed fields stay constant within a thread.
- You continue only from the latest response
id, aftermessage_stopwhen streaming. - Your code branches on
error.details.error_code, not just the HTTP status. - Refusals and fallback-model responses start a new thread.
- No request exceeds 4
cache_controlbreakpoints, top-level included. - Each thread uses one API key and workspace (and one IAM role or user on Claude Platform on AWS).
FAQ
Do threads lower my token bill?
No. Pricing, rate limits, and token usage are unchanged; prompt caching is what lowers cost.
Can I branch from an earlier turn?
Not within a thread. Start a new one, for example from the helper’s thread.params with messages cut back.
Does Claude Code use message threads?
Anthropic says they are on by default for most Claude Code users. For this month’s other Claude Code changes, see our November 2026 roundup.
Sources
- Message threads, Claude Platform documentation
- Release notes, November 2, 2026 entry, Claude Platform documentation
- Make long-running agents faster with message threads, Anthropic, November 2, 2026
- Prompt caching, Claude Platform documentation