Message threads: send only the new turn, and get the first token back faster
In brief
On November 2, 2026 Anthropic released message threads in beta on the Messages API. The API holds the conversation for a few minutes, so each later request carries only the new messages and the id of the previous response. Token cost stays the same; time to first token drops, by about 35% at the median for Claude Code requests between 500k and 1M tokens. How it works, working code, the rules that return 400 errors, and the replay path you have to build.
Contents
On November 2, 2026 Anthropic released message threads in beta on the Messages API.
The Messages API is stateless. Every request carries the whole conversation: the system prompt, the tool definitions, and every earlier turn. For a chat that is a few kilobytes. For a coding agent that has read forty files and taken two hundred tool calls, it is hundreds of thousands of tokens, uploaded, validated, and tokenized again on every single turn. That repeated work shows up as time to first token (TTFT), the wait between sending a request and seeing the first word of the answer.
A message thread keeps the conversation on Anthropic's servers for a short time. You send the full conversation once. After that, each request carries only the new messages plus the id of the previous response, and the API rebuilds the rest before calling the model.
Anthropic's number: Claude Code turned threads on by default, and for requests between 500k and 1M tokens the median TTFT dropped by about 35%. The longer the conversation, the more it helps.
What it changes and what it does not
- Changes: request size, and the time the API spends before the model starts. Screenshots in a computer-use loop and large documents stay with the thread instead of being re-uploaded every turn.
- Does not change: price, rate limits, or token counts. The model still reads the full conversation on every turn, so you are billed for the full input as before.
- Does not cache for you. Prompt caching works exactly as it does without a thread. You still place the breakpoints. Threads and caching stack: caching cuts the cost of the repeated prefix, threads cut the upload and processing time around it.
The three moves
A thread has three operations, set with one new request field, thread:
- Create. Your first request looks like today's, with the full
messages,system, andtools, plus"thread": {"type": "create"}. The API stores the conversation, including Claude's reply, under the responseid. - Continue. Later requests send only the messages since the last response, plus
"thread": {"type": "continue", "previous_message_id": "<last id>"}. Leavesystemandtoolsout; the thread kept them. - Replay. A thread is best-effort. It expires 5 minutes after its last request and is deleted within an hour. When a continue request returns a 404 with
thread_not_found, send the full conversation again in a new create request and carry on from that response'sid.
Every request in a thread needs the beta header anthropic-beta: message-threads-2026-08-12.
The easy way: the SDK thread helper
The SDKs ship a helper that builds the create and continue requests, adds the beta header, and replays for you when a thread is lost:
import anthropic
client = anthropic.Anthropic()
# Creating the helper sends nothing yet.
thread = client.beta.messages.thread(
model="claude-opus-5-5",
max_tokens=1024,
cache_control={"type": "ephemeral"}, # cache each turn (optional)
system="You are a support agent for Acme. Keep replies short.",
)
# Turn 1: creates the thread and sends the full conversation
thread.send({"role": "user", "content": "Hi! I have a question about an order."})
# Turn 2: continues the thread and sends only the new message
second = thread.send(
{"role": "user", "content": "How long does standard shipping usually take?"}
)
print(second.content[0].text)
The TypeScript version is the same shape: client.beta.messages.thread({...}) and await thread.send({...}). If you use the SDK's beta tool runner, it already uses a thread by default.
Doing it by hand
If you call the API directly, the two requests look like this:
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"},
# no system, no tools: the thread holds them
messages=[{"role": "user", "content": "How long does standard shipping usually take?"}],
thread={"type": "continue", "previous_message_id": first.id},
betas=["message-threads-2026-08-12"],
)
In an agent loop, the "new messages" on a continue request are usually the tool_result blocks that answer the tool_use in the previous response.
The field rules that cause 400 errors
Request fields fall into three groups. Getting these wrong is the most likely first bug.
| Group | Fields | Rule |
|---|---|---|
| Create-only | system, tools |
Send on the create request only. Sending either on a continue request, even as null, is a 400. |
| Fixed | model, tool_choice, thinking, mcp_servers, context_management, output_config, container skills, top-level cache_control, the set of beta headers |
Send on every request with the same value. A different value is a 400 thread_fingerprint_mismatch that names the field. |
| Free | max_tokens, stop_sequences, stream, metadata, service_tier, and anything else |
Can change every turn. |
To change a create-only or fixed field (a new tool, a different model), start a new thread with a create request.
Other rules worth knowing before you ship:
- Each response
idcan be continued once. A thread is append-only. To edit or branch an earlier turn, start a new thread. - Do not resend messages the thread already has. The API does not de-duplicate. A continue request that begins with the previous assistant response is an error.
- Streaming: the
idarrives inmessage_start, but continue only aftermessage_stop. If a stream drops, retry the same continue request; if that returnsthread_already_continuedorthread_continue_in_flight, replay. - Cache breakpoints: a thread request can carry at most 4. The top-level
cache_controlcounts as one, and breakpoints onsystemandtoolsfrom the create request count again on every continue. - After a refusal (
stop_reason: "refusal"), do not continue. Start a new thread without the turn that caused it. - Use the same API key and workspace for the whole thread. A request from a different key, user, or workspace gets
thread_not_found.
Build the replay path first
No response tells you whether the API stored that turn. You find out on the next continue request. A thread can also be lost because it expired, because a fallback model answered, or because a response ended in a tool call cut off by max_tokens. The API returns the same 404 in every case and does not say why.
So your application stays the source of truth. Keep your own copy of every message, and write the replay path on day one:
import anthropic
def continue_or_replay(client, history, new_messages, last_id, base):
try:
return client.beta.messages.create(
**base,
messages=new_messages,
thread={"type": "continue", "previous_message_id": last_id},
betas=["message-threads-2026-08-12"],
)
except anthropic.NotFoundError as e:
code = (e.body or {}).get("error", {}).get("details", {}).get("error_code")
if code != "thread_not_found":
raise # a 404 for a bad model name looks similar; check the code
return client.beta.messages.create(
**base,
system=SYSTEM, tools=TOOLS,
messages=history + new_messages, # full conversation from your copy
thread={"type": "create"},
betas=["message-threads-2026-08-12"],
)
Here base holds the fixed fields (model, max_tokens, cache_control, and so on). If your saved history carries cache_control breakpoints on earlier messages, strip them before replaying so you stay within the limit of 4. The SDK helper does that for you, which is the main reason to use it.
What threads do not support
A request with thread is rejected (400 thread_unsupported_request) or unavailable for:
- Message Batches and token counting. Count the full conversation instead.
- Compaction, including compaction on demand. Threads are append-only, so to shrink a long conversation you write the summary yourself and start a new thread from it.
- Context editing, except
clear_thinking_20251015withkeep: "all". - Organizations with HIPAA readiness enabled, or with inference hooks turned on.
There is also no API to list, read, or delete a thread.
Data handling
Threads are not eligible for zero data retention. The conversation, system prompt, and tools are held in memory, encrypted under a server-managed key, never written to durable storage, and deleted no more than an hour after last use. Using threads does not affect your other ZDR-eligible features. If your organization has a ZDR agreement, check with whoever owns it before turning threads on.
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 on Amazon Bedrock or Vertex AI.
Should you turn it on?
- Yes for coding agents, computer-use loops, and document- or image-heavy sessions where the conversation regularly passes 100k tokens and turns come less than five minutes apart.
- Probably not for short chats, batch jobs, anything that relies on compaction, or a single request per conversation. The upload you save is small, and you take on the replay path.
Measure it on your own traffic. The OpenTelemetry export reports per-request latency, so you can compare TTFT with and without threads on the same workload before you roll it out everywhere.
Official docs: Message threads. Announcement: Make long-running agents faster with message threads.