Skip to main content

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

  1. Create: send the full conversation, system, and tools, plus "thread": {"type": "create"}.
  2. Continue: send only the new messages, plus "thread": {"type": "continue", "previous_message_id": "<previous id>"}, without system and tools. Use each new response id for the next turn.
  3. 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

GroupFieldsRule
Create-onlysystem, toolsCreate request only. Sending either on a continue request, even as null, returns a 400 error.
Fixedmodel, 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 valuesSame values on every request. A different value returns a 400 error, thread_fingerprint_mismatch, naming the fields.
Freemax_tokens, stop_sequences, stream, metadata, service_tier, and fields the docs don’t nameCan 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 user message: your next turn, or the tool_result blocks that answer a tool_use.
  • Resume pause_turn with "messages": [] from that response’s id. A new user message 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 the id arrives in message_start. If a continue stream drops, send it again; on thread_already_continued or thread_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.iterations has a fallback_message entry, 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.

Statuserror_codeMeaningWhat to do
404thread_not_foundThe API can’t continue the thread.Send the full conversation in a new create request.
400thread_fingerprint_mismatchA fixed field differs; the message names the fields.Keep fixed fields the same, or start a new thread.
400thread_already_continuedEach response id can be continued once.Continue from the latest id; replay if your client never received that response.
400thread_continue_in_flightAnother continue from this id is still running, usually your own earlier attempt.Let it finish and continue from its id, or replay.
400thread_unsupported_requestA 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 thread carries at most 4 cache_control breakpoints. The top-level one counts, the create request’s system and tools breakpoints count again on every continue, and breakpoints on earlier stored messages don’t. One too many returns thread_unsupported_request.
Message threadsPrompt caching
What it cutsUpload size and per-turn validation and tokenization, for a lower time to first tokenProcessing time and cost, by resuming from a cached prefix
PricingUnchangedCache reads at 0.1× the base input price or less, depending on the model; writes at 1.25× (5-minute) or 2× (1-hour)
LifetimeExpires 5 minutes after the last request5 minutes by default, refreshed on each use; 1-hour option at additional cost
ZDRNot eligibleEligible, 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_20251015 with keep: "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, or model. 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, after message_stop when 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_control breakpoints, 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