← Blog

Claude API Chat History Persistence Design Guide

2026-10-21 · 5 min read · SubToAPI Team

The Claude API is stateless: every request must include the full conversation history you want the model to see, because Anthropic's servers don't remember previous turns on your behalf. Persisting chat history correctly means designing a storage layer that reconstructs the right messages array on every call, keeps costs under control as conversations grow, and survives real-world failure modes like retries, edits, and multi-device sync.

This article covers the practical design decisions: what to store, how to structure it, when to trim or summarize, and how to avoid the two most common bugs — duplicated messages and silently dropped context.

Why statelessness changes your design

Unlike a traditional chat server that keeps session state in memory, Claude API calls are independent HTTP requests. The messages array you send is the entire context the model has. This has three consequences for your persistence layer:

Core data model

A minimal, robust schema has three entities:

conversations (
  id UUID PRIMARY KEY,
  user_id UUID NOT NULL,
  title TEXT,
  system_prompt TEXT,
  created_at TIMESTAMPTZ,
  updated_at TIMESTAMPTZ
)

messages (
  id UUID PRIMARY KEY,
  conversation_id UUID REFERENCES conversations(id),
  role TEXT CHECK (role IN ('user','assistant')),
  content JSONB NOT NULL,
  token_count INT,
  created_at TIMESTAMPTZ,
  sequence INT NOT NULL
)

message_metadata (
  message_id UUID REFERENCES messages(id),
  model TEXT,
  stop_reason TEXT,
  usage JSONB
)

Key design choices here:

Reconstructing the request

When the user sends a new message, your backend typically:

  1. Loads prior messages for that conversation, ordered by sequence.
  2. Appends the new user message.
  3. Builds the messages array in the shape Claude expects (alternating user/assistant roles, each with role and content).
  4. Sends the request, streams or waits for the response, then persists the assistant message before returning it to the client — not after, so a crash mid-stream doesn't lose data.
const history = await db.getMessages(conversationId);
const payload = {
  model: "claude-sonnet-4-5",
  system: conversation.systemPrompt,
  messages: [...history, { role: "user", content: userInput }],
  max_tokens: 1024
};

If you're routing this through SubToAPI instead of managing raw API keys and billing yourself, the request shape is the same — you call https://api.subtoapi.app/v1/messages with your sub_live_... key and the identical messages array. The persistence problem doesn't change; SubToAPI just gives you one HTTPS endpoint, streaming, and usage metadata per call so you can track token consumption per conversation without building your own metering. See /docs/messages for the request format and /docs/streaming for streaming responses.

Handling growth: trimming and summarization

Conversations that run for hundreds of turns will eventually exceed the model's context window or become expensive to resend. Three common strategies, often combined:

A practical middle ground for most apps:

function buildContext(messages, maxTokens = 6000) {
  let total = 0;
  const kept = [];
  for (let i = messages.length - 1; i >= 0; i--) {
    total += messages[i].token_count;
    if (total > maxTokens) break;
    kept.unshift(messages[i]);
  }
  return kept;
}

Run summarization asynchronously once a conversation crosses a size threshold, store the summary as a dedicated message with a type: "summary" tag in its metadata, and exclude the summarized originals from future context builds — but keep them in the database for export and audit.

Handling edits, retries, and branching

Real chat UIs let users edit a past message or regenerate a response. Design for this up front:

Multi-device and team sync

If multiple clients can post to the same conversation (web + mobile, or a team seat), use optimistic locking: include the last-known sequence number in the write request and reject writes that don't match, forcing the client to refetch and retry. This avoids silent message interleaving. SubToAPI's team seat model (/pricing) handles API key and usage separation per user, but conversation-level concurrency control is still your application's responsibility.

Getting started quickly

If you're prototyping this architecture, start with the minimal schema above, a sliding-window context builder, and a single API integration point. /docs/quickstart walks through the first authenticated call, and /docs/tools covers persisting tool_use and tool_result blocks correctly, which follow the same JSONB content pattern as regular messages.

questions

Do I need to send the entire chat history on every API call? Yes — the Claude API has no server-side memory of past turns. You must include every message you want the model to consider in the messages array of each request.

What's the best way to store messages with tool use or images? Use a JSON/JSONB column for message content rather than plain text, since Claude messages can contain multiple content blocks (text, tool_use, tool_result, images) that a flat string can't represent.

How do I keep long conversations affordable? Combine a sliding token-count window with periodic summarization: drop or compress older messages into a single summary message once the conversation exceeds a size threshold, and keep full history in your database for records even if it's excluded from the live context.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →