Claude API Multi-Turn Conversation State Management
What "state management" actually means for the Claude API
The Claude API is stateless. Every request you send is independent — there's no session ID, no server-side memory of what was said before, and no automatic conversation thread. If you want a multi-turn conversation where Claude remembers what the user said three messages ago, you have to send that history back with every single request, in the messages array, in the correct order.
This is the core thing people searching for "multi-turn conversation state management" need to understand: state isn't something the API tracks for you, it's something your application owns. That means you need a strategy for storing message history, deciding what to include when the conversation gets long, and handling things like system prompts, tool calls, and streamed responses consistently across turns. The rest of this article covers exactly that.
The basic shape of a multi-turn request
Each call to the Messages endpoint takes an array of {role, content} objects. To continue a conversation, you append the new user turn and the assistant's prior reply to that array and send the whole thing again:
{
"model": "claude-sonnet-4-20250514",
"max_tokens": 1024,
"messages": [
{ "role": "user", "content": "What's the capital of Portugal?" },
{ "role": "assistant", "content": "Lisbon." },
{ "role": "user", "content": "What's its population?" }
]
}
Claude has no idea "its" refers to Lisbon unless that full history is present. If you only send the last user message, you get a generic or confused answer. This is the single most common bug in "Claude forgot what we talked about" reports — the history simply wasn't sent.
Where to store the history
You have three realistic options, and the right one depends on your architecture:
- Client-side (browser/mobile state) — fine for single-session, throwaway chats like a demo widget. Lost on refresh, not shareable across devices, and exposes your full prompt history to the client.
- Server-side, keyed by session or user ID — the standard approach for anything production-grade. Store messages in Postgres, Redis, or even a flat JSON blob per conversation, and rebuild the
messagesarray server-side before each API call. - Hybrid — client keeps a local copy for instant UI rendering, server keeps the source of truth and validates/rebuilds the array before forwarding to Claude.
A typical server-side schema is just a conversation table and a messages table:
conversations (id, user_id, created_at, title)
messages (id, conversation_id, role, content, created_at, token_count)
On each new user turn: fetch the conversation's messages ordered by created_at, append the new user message, send to the API, append the assistant's reply back to storage. That loop is the entire "state machine."
Managing context window growth
Conversations grow, and so does the token count of everything you resend. Two problems follow: cost (you pay for every input token on every turn) and the model's context window limit. You need a truncation or summarization strategy before either becomes a problem:
- Sliding window — keep only the last N messages (e.g., last 20 turns) and drop older ones. Simple, cheap, but loses early context.
- Summarize-and-replace — periodically ask Claude to summarize the older portion of the conversation into a short system-style message, then drop the raw turns it summarized. Keeps cost flat as conversations grow indefinitely.
- Token-budget trimming — track cumulative token count per message and drop oldest messages once you cross a budget (say, 6000 input tokens), always keeping the system prompt and the most recent few turns intact.
A practical pattern combining the last two:
function buildHistory(allMessages, maxTokens = 6000) {
let total = 0;
const kept = [];
for (let i = allMessages.length - 1; i >= 0; i--) {
total += estimateTokens(allMessages[i].content);
if (total > maxTokens) break;
kept.unshift(allMessages[i]);
}
return kept;
}
Run this before every API call rather than trying to maintain a perfectly trimmed array in storage — keep the full history persisted, and compute the trimmed window on read.
System prompts and tool state across turns
The system parameter is set per-request, not per-conversation, so if your assistant has persona instructions or tool definitions, you resend them every turn too. This is actually useful: it lets you adjust instructions mid-conversation (e.g., switch tone, add a new constraint) without touching stored history.
If you're using tool use, remember that tool call and tool result messages are part of the conversation state as well. A turn where Claude calls a tool produces an assistant message with a tool_use block, and your tool's output goes back in as a user message with a tool_result block. Both need to be stored and replayed in order just like normal text turns — skip a tool result and the next request will fail validation. Our tool use docs cover the exact message shapes if you're combining this with multi-turn state.
Simplifying this with SubToAPI
If you're already managing conversation history, SubToAPI doesn't replace that logic — you still own the messages array — but it removes the surrounding infrastructure: you get a stable sub_live_... API key, usage metadata per request so you can track token growth per conversation, and the same Messages-compatible endpoint so your trimming/storage code doesn't change. Check the quickstart or the Messages API reference to see the request shape, and pricing if you want predictable per-seat costs instead of raw token billing while you build this out.
A minimal multi-turn loop
async function sendTurn(conversationId, userText) {
const history = await loadMessages(conversationId); // from your DB
history.push({ role: "user", content: userText });
const trimmed = buildHistory(history);
const res = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
messages: trimmed
})
});
const data = await res.json();
await saveMessage(conversationId, "assistant", data.content);
return data;
}
Everything else — summarization jobs, token budgets, UI rendering — builds on top of this loop.
questions
Does the Claude API remember previous conversations automatically? No. The API is stateless — it has no memory between requests. Your application must resend the full relevant message history on every call for the conversation to appear continuous.
What's the best way to handle long conversations that exceed the context window? Use a token budget to trim or drop the oldest messages, or periodically summarize older turns into a compact system message and discard the raw text they replaced. Always keep the system prompt and most recent turns intact.
Should I store conversation state on the client or the server? Server-side storage is standard for production apps — it survives refreshes, works across devices, and lets you control what history actually gets sent to the model. Client-side state is fine only for throwaway, single-session demos.