Skip to content

Claude Message Threads Behind an API Gateway ​

Last verified: 2026-11-03 against Anthropic's API release notes and the Message threads docs. The feature is in beta; fields and behavior can change, so check the official docs before you build on it.

Direct answer

On 2026-11-02 Anthropic opened the Message Threads beta on the Messages API (beta header message-threads-2026-08-12). You send the full conversation once. After that, each turn sends only the new messages plus the previous response's id, and the API rebuilds the conversation server-side. That is easy when you call Anthropic directly. Gateways and proxies must meet three hard constraints:

  • Every request of a thread must reach the same key and workspace, or the continue request gets a 404.
  • The thread field must be forwarded verbatim. Dropping it raises no error; the model just answers without the earlier conversation.
  • "Fixed" fields such as model, thinking and the set of anthropic-beta values cannot change during a thread.

Each constraint is covered below, with fixes for the gateway and for the client.

Message Threads in 30 seconds ​

StepWhat you sendNotes
CreateFull conversation, system and tools, plus "thread": {"type": "create"}The API stores the conversation, including the reply, under the response id
ContinueOnly the messages since the previous response, plus "thread": {"type": "continue", "previous_message_id": "<previous id>"}No system or tools: sending either, even as null, returns a 400
ReplayOn 404 thread_not_found, send the full conversation from your own copy in a new create requestThreads are best-effort: they expire 5 minutes after the last request and are deleted within 1 hour

Anthropic positions threads as a way to make long conversations and agent loops send smaller requests and get a lower time to first token: the API no longer re-uploads, re-validates and re-tokenizes the whole conversation on every turn. Three points are easy to misread:

  • Your app is still the source of truth. The docs tell you to keep your own copy of every message and to build the replay path from the start. No response says whether a turn was stored; you find out on the next continue request.
  • A thread is not a cache. In the docs' words, a thread does not cache your prompt for you; you still decide where the cache breakpoints go. See Threads and prompt caching.
  • The docs say nothing specific about billing. This article does not claim threads save money. Go by the usage object in each response.

Anthropic's @ClaudeDevs account on X has also said that Claude Code now uses threads, and quoted a sizable time-to-first-token improvement on long-context requests. As of 2026-11-03 the docs do not mention this, so treat it as a single source. The docs do state that the SDK's beta tool runner uses a thread by default; you can turn this off when you create the runner. So your requests may already carry thread even if you never opted in.

Three groups of fields ​

When you debug a gateway, start here. This table summarizes the docs' "What to send on each request" section:

GroupFieldsRule
Create-onlysystem, toolsSend on the create request only; on a continue request they return a 400
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, the set of anthropic-beta valuesSend on every request with the same values as on create. A difference returns 400 thread_fingerprint_mismatch, and the error message names the fields that differ
Freemax_tokens, stop_sequences, stream, metadata, service_tier, etc.May change from turn to turn

Seven ways a gateway breaks threads ​

If you call Anthropic directly through the official SDK's thread helper, there is little to worry about. The problems sit on the client → gateway → upstream path. The routine things a gateway does for load balancing, failover and billing clash with how threads work.

#Routine gateway behaviorEffect on threadsWhat the client sees
1Load-balancing across upstream keys or workspacesA continue request lands on a different key. The docs list "a different organization, workspace, user or service account" as a 404 causeFrequent 404 thread_not_found. The SDK replays and the call succeeds, but every turn re-sends the full conversation, so the thread gains you nothing
2Parsing the body into a fixed struct and re-serializing itWithout a thread field in the struct, the field is silently droppedSee the most dangerous one
3Rewriting or appending anthropic-beta values, or model mapping / automatic downgradeThe beta value set and model are fixed fields, so any change between turns conflicts400 thread_fingerprint_mismatch
4Retrying on another channel or key after an upstream errorEach response id can be continued once, and the first attempt may already have finished or still be running upstream400 thread_already_continued or thread_continue_in_flight; with a new key, #1 as well
5Normalizing upstream errors into the gateway's own formaterror.details.error_code is lost, so the SDK cannot tell it is thread_not_found and does not replay. A missing model also returns 404, so the status code alone is ambiguousThe 404 reaches application code, or is mistaken for "model not found"
6Exposing only an OpenAI-compatible endpoint and translating to Anthropic internallyChat Completions has no field that can carry threadNot usable; threads need the native Anthropic /v1/messages endpoint
7Estimating usage locally from the request body when the upstream returns noneA continue request carries only the new messages and can be a small fraction of the original size, so local estimates come out far too lowBills that do not match actual usage. The gateway should bill from the upstream usage

The most dangerous one: thread silently dropped ​

Most of these failures raise errors. #2 usually does not.

A continue request is, by design, "only the new messages, no system, no tools". Suppose the gateway deserializes the body into a predefined struct that has no thread field. The field disappears before forwarding, and the upstream receives an ordinary request that holds only the last message or two:

  • If the new messages start with tool_result (the usual case in an agent loop), there is no matching tool_use before them. The upstream returns a 400, so at least you notice.
  • If they start with a plain user message, the upstream answers normally, but with no system prompt, no tools and no earlier context. The reply looks "a bit off" and nothing raises an error.

This is not hypothetical. In the open-source gateway new-api at v1.0.0-rc.46, the anthropic-beta header values are joined and forwarded as-is, but the ClaudeRequest struct has no thread field. On a channel without request-body pass-through, thread is dropped at this step. Any proxy that implements the Anthropic API through typed structs is worth checking for the same problem.

A client-side check: on a continue turn, look for two warning signs. The reply clearly "forgets" the conversation, or the input size in usage (input_tokens + cache_read_input_tokens + cache_creation_input_tokens) is about the size of the few new messages you just sent. Either one strongly suggests the thread is not in effect and the upstream saw only a fragment. The docs do not define how usage is counted under threads, so this is a heuristic: good enough for alerting, not for reconciliation.

Fixing it in the gateway ​

If you maintain a gateway or proxy, this is the minimum set of changes:

  1. Forward thread verbatim. Add the field to your struct, or pass /v1/messages bodies through. If an upstream cannot do threads, return a 400 with an explicit code such as thread_unsupported_request rather than dropping the field. Then the official SDK, with unsupported_request_behavior: continue_unthreaded, falls back to full requests without thread by itself.
  2. Route by previous_message_id. When a create request succeeds, record "response id → upstream channel and key". For a continue request, look up previous_message_id and route it to the same key. A 1-hour TTL is enough, since that is the longest a thread is kept. Record every new response id, because the next turn continues from it.
  3. Never retry a continue request on another key. If the original key fails, return the error to the client so it can replay with a create request. Re-sending the continue request on a different key only turns the failure into a 404 or thread_already_continued.
  4. Pass errors through unchanged. Keep the upstream HTTP status, error.type and error.details, especially error_code and unsupported. The SDK's automatic replay depends on them.
  5. Keep fixed fields stable. Do not append beta values per request. Keep model mapping constant for the life of a thread, and do not downgrade to another model on requests that carry thread.
  6. Bill from upstream usage. Turn off local estimation for requests with thread, or flag a missing upstream usage as an anomaly.
bash
# Create: full conversation + system + tools
curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -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 code reviewer.",
       "messages":[{"role":"user","content":"Review this diff: ..."}],
       "thread":{"type":"create"}}'

# Continue: new messages only; no system/tools; same fixed fields (model, top-level cache_control, beta header)
curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -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":"Now check the error handling."}],
       "thread":{"type":"continue","previous_message_id":"'"$MESSAGE_ID"'"}}'

Through a gateway, replace https://api.anthropic.com with the gateway's base URL and send the same requests. The gateway's job is to make sure the second request reaches the same key as the first.

Defending on the client ​

  • Use the official SDK's thread helper. It:
    • adds the beta header;
    • leaves out system and tools on continue requests;
    • replays once on thread_not_found, thread_already_continued or thread_continue_in_flight;
    • starts a new thread after a fallback-model response or a refusal.
  • Set unsupported_request_behavior to continue_unthreaded. When a gateway or an organization setting rejects threads, the helper falls back to plain requests instead of replaying on every turn.
  • Keep your own copy of the conversation. The helper holds it in memory only. If a session must outlive the process, persist it yourself.
  • When streaming, continue only after message_stop. The id arrives in message_start, but continuing early can return a 404. If a stream drops, send the same continue request once more. If that returns thread_already_continued or thread_continue_in_flight, replay the full conversation.
  • Resume pause_turn with an empty list. When a response ends with stop_reason: "pause_turn", continue with "messages": []. Do not send a new user message while the turn is paused.

Threads and prompt caching ​

Because a continue request carries only the new messages, many people assume caching no longer matters. The docs say otherwise: a thread does not cache for you.

  • The simplest approach is to send the same top-level cache_control on every request. The API places the breakpoint on the last cacheable block of each request, so each turn reads the earlier conversation from cache and writes the new turn to it. The top-level cache_control is itself a fixed field.
  • A request with thread can carry at most 4 breakpoints, and the top-level cache_control counts as one. Breakpoints on the create request's system and tools count again on every continue request. One too many returns 400 thread_unsupported_request.
  • Once a breakpoint is sent it cannot be moved or removed, because a continue request cannot resend earlier messages. Before a replay, strip breakpoints from older messages and keep one on the last block.

The cache economics are unchanged. On Anthropic, a cache write costs 1.25x the input price with a 5-minute TTL or 2x with a 1-hour TTL, and a cache read costs 0.1x. With breakpoints in the wrong place and a low hit rate, you pay more than you would without caching. See Prompt caching explained for how it works and how to do the math. For gateways, note that caches are also scoped to the upstream account. The key affinity from #1 also decides your cache hit rate: fix it once and you solve both problems.

Where threads don't work ​

Per the official docs (2026-11-03):

  • Message Batches: an item with thread ends with invalid_request_error in the results.
  • Token counting: a request with thread returns a 400. Count the full conversation, with system and tools, instead.
  • Compaction: compaction on demand does not work with threads. To compact, write a summary yourself and start a new thread from it.
  • Context editing: only clear_thinking_20251015 with keep: "all" is supported.
  • Organization settings: organizations with HIPAA readiness or Inference hooks enabled cannot use threads, and the feature is not ZDR eligible.
  • Platforms: the Claude API, Claude Platform on AWS, Google Cloud (Anthropic managed) and Microsoft Foundry. Amazon Bedrock is not listed.
  • No management API: you cannot list, read or delete a thread.

Sources ​

The claims that Claude Code already uses threads, and the specific time-to-first-token figures, come only from posts on Anthropic's X account; the docs do not cover them yet. This article does not treat them as settled.

FAQ ​

What are Claude Message Threads? ​

A Messages API beta Anthropic opened on 2026-11-02 (beta header message-threads-2026-08-12). The first turn carries the full conversation with thread: {type: "create"}. Each later turn sends only the new messages with thread: {type: "continue", previous_message_id}, and the API rebuilds the conversation server-side. A thread expires 5 minutes after its last request.

Why do continue requests return 404 thread_not_found through a gateway? ​

The docs list these causes: the thread expired, the previous turn was not stored, a fallback model wrote it, it is still generating, or the request comes from a different organization, workspace, user or service account. A gateway that rotates across upstream keys hits the last one. Route each continue request to the same key by previous_message_id.

Do I still need prompt caching with a thread? ​

Yes. A thread does not cache for you. Sending the same top-level cache_control on every request is the simplest approach. A request with thread can carry at most 4 breakpoints.

What happens if a gateway drops the thread field? ​

The upstream receives an ordinary request with no prior context, no system prompt and no tools. If it starts with tool_result you get a 400. If it starts with a user message, the model answers without context and nothing raises an error.

When can I not use Message Threads? ​

  • Message Batches, token counting, compaction on demand and most context editing.
  • Organizations with HIPAA readiness or Inference hooks enabled.
  • ZDR requirements.
  • Amazon Bedrock.

Got a problem? Contact support or join our group.