
Claude Message Threads: Node.js Setup, Replay and Limits
Claude message threads send only new turns to the Messages API. Node.js setup, field rules, replay on thread_not_found, caching and when to skip them.
- What: Claude message threads keep your conversation on Anthropic’s servers, so each turn sends only new messages.
- Why it matters: Long agent loops stop re-uploading hundreds of kilobytes of history before every model call.
- What to do: Adopt threads behind a flag, keep your own durable copy, and build the replay path first.
- Key stat: Anthropic reports about 35% lower median time to first token on 500K–1M token requests.
Claude message threads are a Messages API beta that stores a conversation server-side between turns. You send the full history once with thread: {type: "create"}, then only new messages plus the previous response ID. Threads cut request size and time to first token, but not token cost, and expire after five idle minutes.
Claude message threads are a beta feature of Anthropic’s Messages API, enabled with the message-threads-2026-08-12 beta header and a thread request field. The API holds the conversation, system prompt and tools in encrypted memory for a short time, rebuilds the full context on each continue request, and returns an ordinary message response.
Claude message threads landed in public beta on November 2, 2026, and they target a cost every agent builder pays without noticing. A coding agent might make hundreds of requests in one session. Each one re-sends every earlier message and tool result. The official docs say the API then “validates and tokenizes all of it again” on every turn.
The feature is simple to switch on. Running it safely in production is the harder part. Threads are best-effort, append-only and short-lived. Most write-ups show the happy path with the SDK helper. Few show what happens in a queue worker or serverless function, where the helper’s in-memory copy disappears with the process.
This guide covers the request rules, a Node.js/TypeScript wrapper with a durable store and replay, and a small payload test we ran on Node 22. It also includes a decision table that compares threads with prompt caching and OpenAI’s previous_response_id.
What are Claude message threads and how do they work?
Threads let the API remember the conversation, so later turns carry only the delta. The docs describe three request types:
- Create. Send the full conversation,
systemandtools, plus"thread": {"type": "create"}. The API stores everything under the responseid. - Continue. Send only the new messages and
"thread": {"type": "continue", "previous_message_id": "<id>"}. Leavesystemandtoolsout. - Replay. If the API can no longer continue, a continue request returns 404. Send the full conversation again in a new create request.
A thread expires 5 minutes after its last request, and the API deletes it no more than 1 hour after last use. Each stored response restarts the five-minute clock. Threads run on four platforms: the Claude API, Claude Platform on AWS, Claude Models on Google Cloud (Anthropic managed) and Microsoft Foundry, per the launch post.
Anthropic says threads are already on by default for most Claude Code users.
How do you use Claude message threads in Node.js?
The fastest way to use Claude message threads in Node.js is the SDK’s client.beta.messages.thread() helper. It adds the beta header, creates the thread on turn one, and continues it afterwards. This is the TypeScript shape from the official example:
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
// Creating the helper sends no request.
const thread = client.beta.messages.thread({
model: "claude-opus-5-5",
max_tokens: 1024,
cache_control: { type: "ephemeral" }, // optional: cache each turn
system: "You are a support agent for Acme. Keep replies short.",
});
// Turn 1: creates the thread and sends the full conversation
await thread.send({ role: "user", content: "Hi! I have a question about an order." });
// Turn 2: 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 helper exists in every official SDK, including Python, Go, Java, C#, Ruby and PHP. Python uses client.beta.messages.thread(...) with the same fields. PHP developers close a stream with close() and read the last turn with $thread->lastMessage(), per the helper notes.
Sending the thread field yourself
If you use the plain create method, you add the thread field and the beta header to every request yourself:
// Turn 1: full conversation + thread: create
const first = await client.beta.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
cache_control: { type: "ephemeral" },
system: SYSTEM_PROMPT,
tools: TOOLS,
messages: history,
thread: { type: "create" },
betas: ["message-threads-2026-08-12"],
});
// Turn 2: ONLY the new messages, no system, no tools
const second = await client.beta.messages.create({
model: "claude-opus-5-5", // fixed: must match create
max_tokens: 2048, // free: may change per turn
cache_control: { type: "ephemeral" }, // fixed
messages: [toolResultMessage],
thread: { type: "continue", previous_message_id: first.id },
betas: ["message-threads-2026-08-12"],
});
My recommendation: start with the helper for interactive tools. Drop to raw requests only when you need your own persistence, which most server-side agents do.
Which fields can change between thread turns?
Only “free” fields such as max_tokens, stream, stop_sequences and metadata can change between turns. The field rules split request fields into three groups, and getting them wrong is the most common source of 400 errors.
- Create-only:
systemandtools. Sending either on a continue request, even asnull, returns 400. - Fixed:
model,tool_choice,thinking,output_config, top-levelcache_controland the set of beta headers. A different value returnsthread_fingerprint_mismatch. - Free: everything else, which can change per turn.
Two more rules catch hand-rolled loops. The API does not de-duplicate your messages, so a re-sent message can appear twice. Each response id can also be continued only once, because a thread is append-only. To edit or branch history, you start a new thread.
The docs also flag a subtle one for current models. On Claude Fable 5.1, Mythos 5.1, Opus 5.5, Sonnet 5.5 and Haiku 5.5, changing system or tools invalidates earlier thinking blocks. Strip thinking and redacted_thinking blocks from the conversation before you re-create with new values.
What breaks when a thread expires?
When a thread expires, the next continue request returns 404 with error_code: "thread_not_found", and the recovery docs say plainly that “the response does not say why.” Expiry is only one cause. Others include a response cut off mid tool call by max_tokens, a reply from a fallback model, a different API key or workspace, and adding the first image to a text-only conversation.
The most important line on the page is this: no response “says whether the API stored that turn.” You only find out on the next request. That makes your own copy of the conversation mandatory, not optional.
The serverless and queue-worker gotcha
The SDK helper replays automatically, but it keeps the conversation in memory only while the helper object exists. In a Lambda, a Vercel function or a BullMQ worker, that object dies between invocations. The code below is what that failure looks like:
// BROKEN: works in the demo, fails in production
let prevId: string | undefined;
async function turn(newMsg: MessageParam) {
const res = await client.beta.messages.create({
model: "claude-opus-5-5",
max_tokens: 1024,
system: SYSTEM_PROMPT, // 400 on a continue request
messages: [newMsg],
thread: prevId
? { type: "continue", previous_message_id: prevId }
: { type: "create" },
betas: ["message-threads-2026-08-12"],
});
prevId = res.id; // no local copy of the conversation
return res; // expiry = unrecoverable 404
}
It has two bugs. It sends system on continue requests, and it stores only the response ID. After a six-minute pause for a slow test run or a human approval, it has nothing to replay. Gabriel Anhaia’s DEV write-up makes the same point: treat the thread as a cache in front of a source of truth you own.
How do you build a durable replay path?
Build the replay path by persisting every message yourself and re-creating the thread from that copy whenever a continue request fails with a replayable error. The fixed version below works with any store, such as MongoDB or Postgres:
import Anthropic from "@anthropic-ai/sdk";
import type { BetaMessageParam } from "@anthropic-ai/sdk/resources/beta/messages";
const client = new Anthropic();
const BETA = "message-threads-2026-08-12";
// Your source of truth: MongoDB, Postgres, Redis... anything durable.
interface ConversationStore {
load(id: string): Promise<{ messages: BetaMessageParam[]; prevId?: string }>;
save(id: string, messages: BetaMessageParam[], prevId?: string): Promise<void>;
}
const FIXED = {
model: "claude-opus-5-5",
cache_control: { type: "ephemeral" as const },
};
export async function threadedTurn(
store: ConversationStore,
convId: string,
newMessages: BetaMessageParam[],
system: string,
tools: Anthropic.Beta.BetaTool[],
) {
const { messages, prevId } = await store.load(convId);
const create = () =>
client.beta.messages.create({
...FIXED, max_tokens: 2048, system, tools,
messages: [...messages, ...newMessages],
thread: { type: "create" }, betas: [BETA],
});
let res;
if (!prevId) {
res = await create();
} else {
try {
res = await client.beta.messages.create({
...FIXED, max_tokens: 2048,
messages: newMessages, // only what is new
thread: { type: "continue", previous_message_id: prevId },
betas: [BETA],
});
} catch (err: any) {
const code = err?.error?.error?.details?.error_code;
const replayable = ["thread_not_found", "thread_already_continued",
"thread_continue_in_flight"];
if (!replayable.includes(code)) throw err; // fingerprint mismatch etc.
res = await create(); // replay from your copy
}
}
// A refusal must not be continued: drop prevId so the next turn re-creates.
const nextPrev = res.stop_reason === "refusal" ? undefined : res.id;
await store.save(convId,
[...messages, ...newMessages, { role: "assistant", content: res.content }],
nextPrev);
return res;
}
The wrapper follows the documented rules. It replays on the three error codes the SDK helper also replays on. It lets thread_fingerprint_mismatch surface as a bug. It never continues after a refusal, which the docs forbid because the refused turn may already be stored.
What we measured
We ran a small payload test on Node.js 22.23.4 in our sandbox. A local mock server mimicked the create/continue/404 semantics. The loop ran 40 turns, each adding an 8 KB tool result, with a 4.8 KB system prompt. We injected one six-minute pause at turn 25 to force a replay:
$ node --version
v22.23.4
$ node bench.mjs
no-thread : total 7693 KB, turn 40 body 369.0 KB
thread : total 607 KB, turn 40 body 9.0 KB, replays 1
The final request shrank from 369.0 KB to 9.0 KB, and total upload fell from 7,693 KB to 607 KB, including the one replay. These are bytes on the wire only, from our mock run. They say nothing about tokens, billing or model latency.
Streaming rules
With streaming, the response id arrives in message_start. You must wait for message_stop before continuing, or you may get a 404, per the streaming section. If a stream drops, retry the same continue once, then replay.
Do Claude message threads reduce token cost?
No. Threads do not reduce token cost: “Threads do not change pricing or rate limits,” according to the pricing section. The launch post adds that Claude “still reads the full conversation on every turn, so token usage stays the same.”
Cost savings still come from prompt caching, and a thread does not cache for you. Send a top-level cache_control on every request, or place breakpoints yourself. A threaded request can carry at most 4 cache_control breakpoints, and a fifth returns thread_unsupported_request.
The prompt caching docs list a 512-token minimum cacheable length for Opus 5.5, Sonnet 5.5 and Haiku 5.5. Cache hits on Opus 5.5 and Sonnet 5.5 are billed at 0.05x the base input price. Threads plus caching is the combination to aim for: smaller uploads and cheaper reads.
For a deeper look at when a single long-running agent beats several short ones, see our guide to AI agent orchestration.
When should you skip message threads?
Skip threads when you need zero data retention, HIPAA, batches or server-side compaction. The unsupported features list is specific:
- Threads are not ZDR eligible or HIPAA eligible. Organizations with HIPAA readiness or Inference hooks turned on are rejected.
- Message Batches, token counting and compaction on demand do not accept
thread. - Context editing is out, except
clear_thinking_20251015withkeep: "all". - There is no API to list, read or delete a thread.
Short chats gain little, because the re-send cost grows with conversation length. Threads also add failure modes to agents with long idle gaps. If your agent often waits more than five minutes for a human, expect a replay on most turns.
If you are running agents locally, our local AI coding agent setup guide for macOS covers the tooling side.
| Approach | What it reduces | Server-side lifetime | Billing effect | ZDR |
|---|---|---|---|---|
| Full resend (no thread) | Nothing | None | Standard input tokens | Depends on other features |
| Claude message threads | Request size, time to first token | 5 min idle, deleted within 1 hour | No change | Not eligible |
| Claude prompt caching | Input cost, latency on cached prefix | 5 min default, 1 hour option | Cache hits 0.05x base input on Opus 5.5 and Sonnet 5.5 | Eligible |
| OpenAI previous_response_id | Request size | Response objects kept 30 days by default | All prior input tokens in the chain billed as input | Requires store; see OpenAI docs |
The NexGismo verdict: Message threads are worth adopting for long agent loops that stay busy, such as coding and computer-use agents, as long as you pair them with prompt caching. I’d ship them behind a feature flag, with your own durable conversation store and the replay path written before the happy path. For short chats, idle-heavy approval flows or anything under ZDR or HIPAA, stay unthreaded.
- Claude message threads entered public beta on November 2, 2026 under the
message-threads-2026-08-12beta header. - A thread expires 5 minutes after its last request and is deleted within 1 hour.
- Threads cut request size and time to first token but leave token pricing and rate limits unchanged.
- Continue requests must omit
systemandtoolsand keep fixed fields likemodelidentical. - Any 404
thread_not_foundmeans replaying the full conversation from a copy your application stores. - Threads are not ZDR or HIPAA eligible and do not work with Message Batches or compaction on demand.
Frequently Asked Questions
Do Claude message threads reduce token cost?
No. Anthropic says threads do not change pricing or rate limits, and Claude still reads the full conversation on every turn, so token usage stays the same. Threads reduce request size and time to first token. To lower cost, combine threads with prompt caching by sending a top-level cache_control on every request of the thread.
How long does a Claude message thread last?
A thread expires 5 minutes after its last request, and each stored response restarts that timer. The API deletes the thread no more than 1 hour after its last use. After expiry, a continue request returns a 404 thread_not_found error, and you must replay the full conversation in a new create request.
What is the difference between message threads and prompt caching?
Message threads store the conversation on the server so you send only new messages, which shrinks uploads and time to first token. Prompt caching stores processed prompt prefixes so cached input is billed at a fraction of the base price. Threads do not cache for you, so you use both together for speed and cost savings.
Are Claude message threads ZDR or HIPAA eligible?
No. Anthropic lists message threads as not eligible for zero data retention and not HIPAA eligible. Organizations with HIPAA readiness or Inference hooks turned on get a thread_unsupported_request error. The API holds threads in encrypted memory, never writes them to durable storage, and deletes them within an hour of last use.
What happens when a Claude message thread expires?
The next continue request fails with HTTP 404 and error code thread_not_found, and the response does not say why. Recover by sending the full conversation from your own stored copy, with system and tools, in a new create request, then continue from the new response id. The SDK helper does this once automatically.
Should you switch your agent loop to threads?
Claude message threads solve a real problem in long agent sessions: re-uploading and re-tokenizing history that has not changed. The win grows with context size, and Anthropic’s own figure applies to very long requests only. The cost is a new failure surface: short expiry, opaque 404s, strict field rules and no ZDR. Treat the thread as a cache, keep the canonical conversation in your own store, and test the replay path as hard as the happy path.
If you are designing the wider system around agents like this, our piece on AI solution architect vs system architect skills covers the architecture thinking behind these trade-offs. Try threads on one long-running agent first, measure your own time to first token, and expand from there.