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
threadfield must be forwarded verbatim. Dropping it raises no error; the model just answers without the earlier conversation. - "Fixed" fields such as
model,thinkingand the set ofanthropic-betavalues cannot change during a thread.
Each constraint is covered below, with fixes for the gateway and for the client.
Message Threads in 30 seconds
| Step | What you send | Notes |
|---|---|---|
| Create | Full conversation, system and tools, plus "thread": {"type": "create"} | The API stores the conversation, including the reply, under the response id |
| Continue | Only 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 |
| Replay | On 404 thread_not_found, send the full conversation from your own copy in a new create request | Threads 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
usageobject 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:
| Group | Fields | Rule |
|---|---|---|
| Create-only | system, tools | Send on the create request only; on a continue request they return a 400 |
| 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, the set of anthropic-beta values | Send 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 |
| Free | max_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 behavior | Effect on threads | What the client sees |
|---|---|---|---|
| 1 | Load-balancing across upstream keys or workspaces | A continue request lands on a different key. The docs list "a different organization, workspace, user or service account" as a 404 cause | Frequent 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 |
| 2 | Parsing the body into a fixed struct and re-serializing it | Without a thread field in the struct, the field is silently dropped | See the most dangerous one |
| 3 | Rewriting or appending anthropic-beta values, or model mapping / automatic downgrade | The beta value set and model are fixed fields, so any change between turns conflicts | 400 thread_fingerprint_mismatch |
| 4 | Retrying on another channel or key after an upstream error | Each response id can be continued once, and the first attempt may already have finished or still be running upstream | 400 thread_already_continued or thread_continue_in_flight; with a new key, #1 as well |
| 5 | Normalizing upstream errors into the gateway's own format | error.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 ambiguous | The 404 reaches application code, or is mistaken for "model not found" |
| 6 | Exposing only an OpenAI-compatible endpoint and translating to Anthropic internally | Chat Completions has no field that can carry thread | Not usable; threads need the native Anthropic /v1/messages endpoint |
| 7 | Estimating usage locally from the request body when the upstream returns none | A continue request carries only the new messages and can be a small fraction of the original size, so local estimates come out far too low | Bills 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 matchingtool_usebefore them. The upstream returns a 400, so at least you notice. - If they start with a plain
usermessage, 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:
- Forward
threadverbatim. Add the field to your struct, or pass/v1/messagesbodies through. If an upstream cannot do threads, return a 400 with an explicit code such asthread_unsupported_requestrather than dropping the field. Then the official SDK, withunsupported_request_behavior: continue_unthreaded, falls back to full requests withoutthreadby itself. - Route by
previous_message_id. When a create request succeeds, record "responseid→ upstream channel and key". For a continue request, look upprevious_message_idand route it to the same key. A 1-hour TTL is enough, since that is the longest a thread is kept. Record every new responseid, because the next turn continues from it. - 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. - Pass errors through unchanged. Keep the upstream HTTP status,
error.typeanderror.details, especiallyerror_codeandunsupported. The SDK's automatic replay depends on them. - 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. - Bill from upstream
usage. Turn off local estimation for requests withthread, or flag a missing upstreamusageas an anomaly.
# 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
systemandtoolson continue requests; - replays once on
thread_not_found,thread_already_continuedorthread_continue_in_flight; - starts a new thread after a fallback-model response or a refusal.
- Set
unsupported_request_behaviortocontinue_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. Theidarrives inmessage_start, but continuing early can return a 404. If a stream drops, send the same continue request once more. If that returnsthread_already_continuedorthread_continue_in_flight, replay the full conversation. - Resume
pause_turnwith an empty list. When a response ends withstop_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_controlon 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-levelcache_controlis itself a fixed field. - A request with
threadcan carry at most 4 breakpoints, and the top-levelcache_controlcounts as one. Breakpoints on the create request'ssystemandtoolscount again on every continue request. One too many returns 400thread_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
threadends withinvalid_request_errorin the results. - Token counting: a request with
threadreturns a 400. Count the full conversation, withsystemandtools, 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_20251015withkeep: "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
- Anthropic API release notes (2026-11-02, Message threads): https://platform.claude.com/docs/en/release-notes/api
- Message threads docs: https://platform.claude.com/docs/en/build-with-claude/message-threads
- Prompt caching docs: https://platform.claude.com/docs/en/build-with-claude/prompt-caching
- new-api source (v1.0.0-rc.46, the
ClaudeRequeststruct and the Claude channel adaptor): https://github.com/QuantumNous/new-api
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.
