· 6 min read
Claude message threads: server-side conversations explained
Claude message threads keep a conversation on the server, so each turn sends only new messages. How create, continue, expiry and caching work.

Claude message threads are a beta feature of the Messages API, added on November 2, 2026, that keep a conversation on Anthropic's servers for a short time. You send the full conversation once, and on every later turn you send only the new messages plus the id of the previous response. It is turned on with the message-threads-2026-08-12 beta header and does not change pricing or rate limits.
Until now the Messages API was fully stateless: every request carried the whole history, system prompt and tool definitions. Threads remove that resend for the life of a conversation. Here is how they work, what stays fixed between turns, and where they can trip you up.
What are Claude message threads?
A message thread is a short-lived, server-side copy of a conversation, keyed by the id of the most recent response. You opt in with one new request parameter, thread, which takes one of two shapes:
{"type": "create"}on the first request, which carries the full conversation, thesystemprompt and thetools.{"type": "continue", "previous_message_id": "<id>"}on every later request, which carries only what is new since that response.
The response itself is the normal message object. Nothing about how you read content blocks changes.
Threads are available on the Claude API, Claude Platform on AWS, Claude Models on Google Cloud (Anthropic managed) and Claude in Microsoft Foundry. They are not eligible for zero data retention (ZDR) or HIPAA, so check that before using them on regulated data.
How do you create and continue a thread?
The first turn looks like any request, with thread added. The second turn drops system and tools and points at the first response:
import anthropic
client = anthropic.Anthropic()
first = client.beta.messages.create(
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"},
betas=["message-threads-2026-08-12"],
)
second = client.beta.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
cache_control={"type": "ephemeral"},
messages=[{"role": "user", "content": "How long does standard shipping take?"}],
thread={"type": "continue", "previous_message_id": first.id},
betas=["message-threads-2026-08-12"],
)
The new messages usually start with a user turn. In a tool loop, that turn holds the tool_result blocks answering the tool_use in the previous response. If a response stops with pause_turn, you resume by continuing from its id with an empty messages list, and you should not add a new user message while the turn is paused.
The Python SDK also has a helper that tracks the ids for you. client.beta.messages.thread(...) takes the model, system prompt and other settings without sending anything, and each thread.send(message) creates or continues the thread as needed.
Which request fields can change between turns?
The API sorts request fields into three groups, and this is the part most worth memorising:
| Group | Fields | Rule |
|---|---|---|
| Create-only | system, tools | Sent on create; sending them on continue, even as null, returns 400 |
| Fixed | model, tool_choice, thinking, mcp_servers (except auth tokens), context_management, output_config, container skills, top-level cache_control, the set of beta headers | Must match every turn, or you get 400 thread_fingerprint_mismatch |
| Free | max_tokens, stop_sequences, stream, metadata, service_tier and others not listed | Can change per turn |
In practice this means a thread is bound to one model and one thinking configuration. If your app switches models mid-conversation, say a cheaper model for small talk and a larger one for hard questions, each switch has to start a new thread.
Some things are deliberately not stored. MCP server URLs, MCP authorization tokens and the code execution container id must be sent on every request that uses them. Files are held by reference, so every file the conversation uses has to stay available while the thread is in use.
A thread is also append-only. Each response id can be continued exactly once; a second continue from the same id returns thread_already_continued. To edit an earlier turn or branch the conversation, you start again with a new create request.
How long does a thread last?
A thread expires five minutes after its last request, and each stored response restarts that timer. The API deletes it no later than an hour after its last use. Once a thread can no longer be continued, a continue request returns a 404 with the error code thread_not_found.
That short window drives the main design rule: keep your own copy of the conversation. The response never tells you whether a turn was stored; you find out on the next continue. The recovery path is always the same: send the full history in a new create request, then continue from that response's id.
def send_turn(client, state, new_messages):
try:
resp = client.beta.messages.create(
**state.fixed_params,
messages=new_messages,
thread={"type": "continue", "previous_message_id": state.last_id},
betas=["message-threads-2026-08-12"],
)
except anthropic.NotFoundError:
# Thread expired: replay the full history as a new thread
resp = client.beta.messages.create(
**state.fixed_params,
system=state.system,
tools=state.tools,
messages=state.history + new_messages,
thread={"type": "create"},
betas=["message-threads-2026-08-12"],
)
state.history += new_messages + [{"role": "assistant", "content": resp.content}]
state.last_id = resp.id
return resp
Treat that sketch as a shape, not a drop-in: a production version would check the error code, not just the status, and handle the other thread errors below the same way.
Streaming has its own rule. The response id arrives in the message_start event, but you should continue from it only after message_stop. If a continue stream drops early, resend the same continue request; if that returns thread_already_continued or thread_continue_in_flight, replay the history in a new create request.
Does a thread cache the prompt for you?
No. A thread saves you from resending tokens over the network, but the model still processes the whole conversation each turn, and you still place cache breakpoints yourself. To cache every turn, send a top-level cache_control on every request in the thread, as in the example above. If you have tuned breakpoints before, as in prompt caching on GPT-6, the logic carries over.
There is a limit to watch: a request with thread can carry at most four cache_control breakpoints, and the top-level one counts as one of them. Breakpoints you put on system or tools in the create request count again on every continue request. Going over returns 400 thread_unsupported_request, and breakpoints cannot be moved or removed once sent.
Key takeaways
- Claude message threads keep a conversation server-side, so later turns send only new messages and
previous_message_id. systemandtoolsgo on create only; model, thinking and tool choice are fixed for the thread's life.- Threads expire five minutes after the last request, so always keep a local history and replay it on
thread_not_found. - Each response
idcontinues once; branching or editing means a new thread. - Threads do not cache for you, and pricing and rate limits are unchanged.
FAQ
Do Claude message threads reduce cost?
Not by themselves. Pricing is unchanged, and the model still reads the full conversation each turn. Savings come from prompt caching, which you still configure with cache_control.
Can you switch models inside a thread?
No. model is a fixed field, so a different value returns thread_fingerprint_mismatch. Start a new thread with the full history to change models.
What happens if a thread expires mid-conversation?
The continue request returns 404 with thread_not_found. Send the full conversation in a new create request and continue from there.