CCD-F.1 · D2 + D6 + D5 · ProcessCCD-F D2 · 33%20 min build

Production Claude Chat Application.

A streaming chat backend on the Messages API. The server owns conversation history (the API is stateless), renders content_block_delta events to the client as they arrive, appends the full response.content (not just the text) back onto history so tool/thinking blocks survive, and manages the context window with token counting plus compaction rather than an unbounded array. Prompt caching on the system prompt cuts the repeated-context cost that dominates long conversations.

Loading your plan…
Mental modelArchitecture chooses the flow. Deterministic controls enforce policy. Structured state preserves truth.
Loop mascot illustrating Production Claude Chat Application.
Share
01 · System & parts

What this system is, and its 4 parts

This is the app most developers build first: a chat UI backed by Claude, with responses that stream in word by word instead of appearing all at once after a long wait. It exists because a 6-second pause before any text appears feels broken to a user, while the same 6 seconds spent watching words stream in feels fast. The hard part isn't calling the API, it's everything around the call: keeping the conversation history somewhere durable, knowing when the context window is about to overflow, and making sure a client disconnect mid-stream doesn't silently lose the assistant's answer.

4Components
D2Primary domain
6Build steps
8Decision traps
5Concept links
Stack · Python or TypeScript SDK - session store (DB or Redis)Needs · Messages API basics - stop_reason - SSE
Production Claude Chat Application component architecture.
4 components. Each owns one concept.
01

Stream Handler

SSE consumer - UI renderer

Opens client.messages.stream(), listens for text deltas, and writes them to the client connection as they arrive instead of buffering the whole response.

Configurationclient.messages.stream({model, max_tokens, system, messages}) with a text-delta listener; call .finalMessage() once to get the complete Message for persistence.
Concept: streaming →
02

Session Store

server-owned history

Persists the full message array per session_id in a database. The API is stateless - every request resends the whole history, so this store is the only source of truth.

Configurationsessions[session_id].messages.push({role: 'assistant', content: response.content}) - the full content array, never just the extracted text.
Concept: session-state →
03

Context Budget Manager

count_tokens + compaction

Calls messages.count_tokens() before each turn against the live model. When the count nears the working budget, trims oldest turns or engages compaction rather than sending an oversized request.

Configurationif count_tokens(messages).input_tokens > BUDGET: trim oldest non-system turns, or (beta) enable context_management compaction with beta compact-2026-01-12.
Concept: context-window →
04

Cache Breakpoint

system prompt - ephemeral

Marks the last block of the (stable, non-interpolated) system prompt with cache_control so repeated turns in the same conversation read from cache instead of reprocessing the full prefix.

Configurationsystem: [{type: "text", text: SYSTEM_PROMPT, cache_control: {type: "ephemeral"}}] - SYSTEM_PROMPT never has a timestamp or session id interpolated into it.
Concept: prompt-caching →
02 · Problem framing

The problem

What the user needs
  1. See the response start appearing within ~200ms, not after the full generation finishes.
  2. Never lose an in-progress answer when the browser tab closes or the network blips.
  3. Keep a 40-turn conversation coherent without the context window silently overflowing.
Why naive approaches fail
  1. Client-side-only history means refreshing the page loses the whole conversation (the Messages API is stateless - nothing is stored server-side unless you store it).
  2. Appending only response.text back onto history drops tool_use and thinking blocks, corrupting the next turn's context.
  3. An unbounded messages array eventually exceeds the context window and every request starts failing at once instead of degrading gracefully.
Definition of done
  • ✓ Time-to-first-token < 300ms on a warm cache
  • ✓ Zero conversations lost on client disconnect (server-side persistence, not client memory)
  • ✓ Context budget enforced before the 1M-token ceiling, with graceful compaction
  • ✓ Full response.content persisted per turn, including non-text blocks
03 · Data flow

One run, traced end to end

Production Claude Chat Application sequence diagram.
Production Claude Chat Application end-to-end flow.
04 · Build

6 steps to production

01

Open a stream and render deltas

Use the SDK's stream() helper rather than a manual SSE parser. Only text_delta events touch the UI; other event types drive server-side bookkeeping.

Concept: streaming →
Python
client = Anthropic()

def stream_turn(messages, system_prompt):
    with client.messages.stream(
        model="claude-sonnet-5",
        max_tokens=4096,
        system=[{"type": "text", "text": system_prompt,
                 "cache_control": {"type": "ephemeral"}}],
        messages=messages,
    ) as stream:
        for text in stream.text_stream:
            yield text  # forward to the client connection
        return stream.get_final_message()
02

Persist the full content array, not the text

The final assistant turn must carry every content block, including thinking or tool_use blocks, or the next request loses structure the API needs to stay coherent.

Concept: session-state →
03

Budget the context window before every turn

Call count_tokens against the model you're about to use (token counts are model-specific) and trim or compact before the request, not after it fails.

Concept: context-window →
04

Branch on stop_reason after the stream ends

The stream's final message carries the same stop_reason contract as a non-streaming call. Handle max_tokens and refusal explicitly instead of assuming every stream ends cleanly.

Concept: streaming →
05

Inject runtime context without breaking the cache

On models that support it, append a role: 'system' message to the array for mid-conversation operator context instead of rewriting the top-level system prompt, which would invalidate the cached prefix on every turn.

Concept: prompt-caching →
06

Handle client disconnect without losing the turn

The Messages API stream itself has no server-side replay. Keep the request running server-side even if the client connection drops, and persist on completion regardless of whether anyone was still listening.

Concept: session-state →
05 · Right call, wrong call

8 decisions the exam turns into distractors

01 · Where conversation history lives
Looks right

client-side only (localStorage/component state)

Actually right

server-side session store, keyed by session_id

DEC-01
02 · What gets persisted per turn
Looks right

just the extracted text string

Actually right

the full response.content array

DEC-02
03 · Context overflow handling
Looks right

let the request fail, then handle the error

Actually right

count_tokens before the request, trim/compact proactively

DEC-03
04 · Default model tier for chat traffic
Looks right

claude-opus-4-8 for every message

Actually right

claude-sonnet-5

DEC-04
05 · Content truncation on persist
Looks right

Server stores finalMessage.content[0].text as the assistant turn.

Actually right

Store the full content array. Non-text blocks are silently dropped otherwise, breaking later turns.

CCDF-01
06 · Cache-breaking interpolation
Looks right

System prompt string includes f"Current time: {now()}" at the top.

Actually right

Keep the system prompt byte-identical across requests. Inject dynamic context via a role: 'system' message instead.

CCDF-02
07 · No proactive context budget
Looks right

Messages array grows unbounded until a request 400s on context length.

Actually right

Call count_tokens before every request and trim or compact before it fails, not after.

CCDF-03
08 · Streaming with no persistence path
Looks right

Assistant text is only ever rendered to the open connection, never saved.

Actually right

Always call .finalMessage() / get_final_message() and persist it, independent of whether the client is still connected.

CCDF-04
Production Claude Chat Application failure map.
06 · Budget

Cost & latency

~1,800 input (cached) - 600 outputPer-turn tokens (typical)

System prompt + tool defs read from cache after turn 1; only new user text + growing tail cost full price.

~$0.006Per-turn cost (Sonnet 5)

At $2/$10 intro per MTok with ~70% cache-read rate on the stable prefix.

~200-300msTime-to-first-token

Streaming avoids waiting for the full generation; first token arrives as soon as the model emits it.

07 · Ship checklist

Check every gate before release

0/7 checked
  • session-state →
  • streaming →
  • context-window →
  • prompt-caching →
08 · Practice

4 exam-pattern questions

Work through one question at a time, check the architecture, then move through the set.

Question 1 of 4Check the architecture

Your chat app persists response.content[0].text as the assistant turn. After adding a tool, multi-turn conversations start behaving inconsistently. Why?

09 · FAQ

Frequently asked

Do I need a database, or can I keep history in memory?

Use a database (or Redis with persistence) for anything beyond a local demo. In-memory history is lost on process restart and doesn't scale past one server instance.

Should I stream on every request?

Stream whenever max_tokens is large or the user is waiting live for the response - which is essentially always true for a chat UI. Non-streaming is fine for short, backend-only calls with small max_tokens.

How do I know when to trim history versus use compaction?

Trimming (dropping oldest turns) is simple and works for most chat apps. The beta compaction feature (compact-2026-01-12) is worth adopting when you need the model to retain awareness of dropped history rather than losing it outright - useful for long support or coaching sessions.

What model should a general-purpose chat app default to?

Claude Sonnet 5 for the vast majority of conversational traffic. It is the best cost/quality balance in the current lineup; reserve Opus 4.8 for a router-driven escalation path (see the model-selection scenario), not as the default.

Is it safe to cache the entire conversation, not just the system prompt?

The message array is not typically cached because it grows every turn and each growth invalidates a naive full-array cache marker. Cache the stable prefix (system + tool defs); let the growing message tail be uncached, or use auto top-level caching which places the marker on the last cacheable block.

Help someone build it

Share this scenario.

One share is one less team repeating the same architecture mistake.

Last reviewed ·