Management
The API has zero memory. Every "conversation" you've ever had with Claude is your code re-sending the entire transcript on every turn. Five concepts, from statelessness to a production-grade conversation manager.
The 5 concepts of memory
Tap any concept to jump to its 5-card cluster.
- 1The Stateless RealityClaude has no memory between requests
- 2History PatternsFull history, sliding window, summarization
- 3Token BudgetSplitting 200K across system, history, response
- 4Pruning StrategiesCutting messages without losing key facts
- 5Conversation ManagerA production class for long, persistent chats
Claude has no memory
The Claude Messages API is stateless. There is no session, no server-side storage, no implicit memory of any kind. Every API request is processed in complete isolation from every other request your code has ever made.
What feels like a "conversation" is an illusion built by your application. On every single turn, your code replays the entire transcript — user message 1, assistant reply 1, user message 2, assistant reply 2, and so on — in the messages array. If you leave a turn out, it never happened as far as Claude is concerned.
The amnesiac expert
BEFORE: Picture a world-class consultant with perfect amnesia. Brilliant, fast, articulate — but every time you walk into the room, they have zero memory of any previous meeting.
PAIN: You walk in and say "so what do you think about option B?" They stare blankly. They don't know what option A was, who you are, or what problem you are solving. Every meeting starts from absolute zero.
MAPPING: Each Claude API call is a fresh consultant in a fresh room. The only way to give them "memory" is to hand them a written transcript on the way in. Your messages array is that transcript — and you, the developer, are the one carrying it from meeting to meeting.
Three turns, three full transcripts
- Turn 1Send user 1. Claude responds. Your code stores both in a list. Server forgets the call the moment the response leaves.
- Turn 2Send user 1, asst 1, user 2 — all of it, every byte. Claude responds. Append the reply.
- Turn NSend everything that came before plus the new turn. Token usage grows linearly with the turn count.
- Pop on failureIf a call errors, pop the last user message you appended — otherwise the next call sends two user turns in a row, which the API rejects.
The "memory" you experience in claude.ai is built the exact same way: the client replays the whole transcript on every request.
Where does memory live?
# Question: I want my agent to "remember" X. Where do I put it? IF X must persist across the SAME conversation: PUT X in the messages array # this is the model's only working memory ELIF X must survive a server restart or new device: SAVE messages array to disk / DB / Redis LOAD on next request & resend # the array IS the conversation ELIF X is a long-lived user preference / fact: STORE in your app DB INJECT into system prompt every request # cheaper than carrying it in history forever ELSE (single-call, no follow-up): JUST CALL Claude, no state needed
There is no fourth option. The model itself can't remember anything. Every retention strategy is a way of putting bytes back into the next request.
Misconceptions
messages array doesn't include a turn, it never happened. Every "memory" feature in production is your code replaying past turns.The Messages API is stateless. Memory is your application's job. The messages array IS the model's memory — nothing else persists between calls.
Build conversations by saving messages on your side and resending them. Every other concept in this module is a smarter way of doing that one thing.
Three ways to ship the past
Once you accept that you must resend history, the real question is which history. There are three canonical patterns. Full history sends every turn ever — simplest, but tokens grow forever. Sliding window keeps only the last N turns — fixed cost, but silently drops early facts. Summarization compresses old turns into a short paragraph and keeps recent turns verbatim — the production sweet spot.
Real systems usually combine all three: full history for the last 4 turns, a rolling summary for everything older, and "pinned" facts that never get summarized. This hybrid is what every serious customer-support, healthcare, and coding agent uses under the hood.
Packing for a two-week trip
BEFORE: Imagine packing for two weeks abroad with one carry-on at a strict 7 kg limit. No checked luggage allowed.
PAIN: Bring everything (full history) and the bag overflows at the gate. Bring only what fits today (sliding window) and you arrive at a formal dinner in hiking clothes because you tossed the dress shoes on day three.
MAPPING: The carry-on is your context window. Summarization is the trick the seasoned traveler uses: pack the last few days in full, plus a short note — "left two suits at home, bringing the navy one" — that captures everything else without the bulk. The trip survives. Your context survives.
Three strategies, side by side
After 20 turns, sending turn 21:
- Full historySend all 20 prior messages plus the new one. ~3,000 tokens. Trivial code, breaks past ~100 turns.
- Sliding window (N=6)Send only the last 6 + new. ~1,050 tokens. Fixed cost forever — but turn 1 facts vanish at turn 7.
- SummarizationSend a 1-paragraph summary of turns 1–14, then turns 15–20 verbatim, then the new one. ~1,400 tokens. Bounded cost, key facts preserved.
- Hybrid (production)Pinned facts at top (account #, preferences) · rolling summary · last 4–6 turns full. The shape every serious agent ends up with.
Cost reality: a 50-turn support chat is ~$0.23/reply on full history vs ~$0.03/reply on summarization. At 10,000 chats/day that is $2,000/day saved.
Pseudocode
# A. FULL HISTORY -- send everything, every time FUNCTION full_history(history, new_msg): RETURN history + [new_msg] # B. SLIDING WINDOW -- keep only the last N turns FUNCTION sliding(history, new_msg, n=6): trimmed = history[-n:] RETURN trimmed + [new_msg] # C. SUMMARIZE -- compress old, keep recent FUNCTION summarize_strategy(history, new_msg, keep=4): old = history[:-keep] recent = history[-keep:] IF len(old) > 0: summary = CALL Claude("summarize, keep IDs/names verbatim", old) RETURN [ {role: "user", content: "[SUMMARY] " + summary}, {role: "assistant", content: "Got it."} ] + recent + [new_msg] RETURN recent + [new_msg]
Always end with a real user message and keep the user/assistant alternation valid — the API rejects two user turns in a row.
Misconceptions
Three patterns, one truth: full history, sliding window, and summarization all just decide which subset of past turns ships in the next request.
Default to summarization with a small recent window. It's the only one that scales without losing information — especially when paired with pinned facts.
One window, four tenants
Every Claude request has a fixed total budget — for example, 200,000 tokens. Four things compete for that space: the system prompt, the conversation history, the current user message, and the reserved response (the max_tokens you set). Together they cannot exceed the window.
The math is simple but unforgiving: available = window - system - history - new_msg - max_tokens. Cross zero and the API rejects the request. Worse, even before that limit hits, recall starts degrading because of "lost in the middle" — facts buried in a 180K context get less attention than facts at the start or end.
The fixed-size suitcase
BEFORE: You have a single suitcase, exactly 200,000 cm³ — not one cm more — for an international trip. Everything you need must fit, and you must leave room for souvenirs to bring home.
PAIN: Stuff it with the photo album of every prior trip (uncapped history) and there's no room for today's clothes (current message), let alone the souvenir space (response). At the airport, the bag fails the size check and you get rebooked. In API terms, the request errors and the user gets a spinning circle.
MAPPING: The suitcase is the context window. The travel guide is the system prompt (always packed). The album is conversation history (grows every trip). Today's clothes are the new message. The empty space is max_tokens. Pack like an engineer, not a pack rat.
Worked example, turn 25
- Total window200,000 tokens. The hard ceiling for a single request.
- System prompt1,200 tokens, fixed. Sent on every call — cumulative billing across the whole chat.
- History (25 turns)~37,500 tokens. Grows linearly — every turn adds another user+assistant pair.
- New user message~800 tokens. Variable per turn.
- max_tokens (reserved reply)4,096 tokens. You set this; the model never returns more than this.
- Remaining slack200,000 − 1,200 − 37,500 − 800 − 4,096 = 156,404 tokens. Plenty — today.
By turn 120 the same chat has ~180K of history alone. Slack drops to ~14K and the next reply may truncate. Trim BEFORE the budget runs out, not after.
Pseudocode
# Compute remaining slack before every API call FUNCTION available_for_history(window, sys, msgs, new_msg, max_tok): used = sys + tokens(msgs) + tokens(new_msg) + max_tok RETURN window - used # Pre-flight check: trim BEFORE you call FUNCTION guarded_send(state, new_msg): slack = available_for_history( window=200_000, sys=tokens(state.system), msgs=state.messages, new_msg=new_msg, max_tok=4096 ) IF slack < 10_000: # budget is tight -- compress before sending state.messages = summarize_strategy(state.messages) reply = CALL Claude(state.system, state.messages + [new_msg]) # real-time signal: reply.usage.input_tokens state.last_input_tokens = reply.usage.input_tokens RETURN reply
Treat response.usage.input_tokens as your live budget gauge. When it climbs, trim on the next turn — not after the API returns a 400.
Misconceptions
Four tenants share one window: system + history + current message + reserved max_tokens. Add them up before every call.
Trim early, not late. A trim BEFORE the call costs one extra summary; a trim AFTER a 400 error costs you a failed user request and a debugging session.
Cut the noise, keep the signal
Pruning is the art of removing messages without losing information density. Not all messages are equal — "thanks!" carries no context value, while "my account is #4521" must survive every trim. Smart pruning scores each message by importance and drops the cheap ones first.
Five strategies stack on top of each other: FIFO (drop oldest), importance scoring (drop low-value first), semantic dedup (drop repeated questions), role-based retention (always keep tool results & key decisions), and summarize-and-replace (compress a block before dropping). Production systems combine three or four of them in a single pipeline.
The film editor
BEFORE: You have 6 hours of raw footage that must become a 90-minute movie. Key plot points are scattered throughout — some in the first 10 minutes, some in the final 20.
PAIN: Cut blindly from the start and you lose the character introduction that makes the climax meaningful. Cut blindly from the end and you spoil the ending. Keep everything and the audience checks out during filler scenes.
MAPPING: Pruning a conversation works the same way. Greetings and "thanks" are filler — cut first. Account numbers and tool results are plot points — preserve at all costs. Sometimes you replace a block of scenes with a short "previously on this conversation..." narration. Importance score is the editor's gut.
The five-strategy stack
- FIFODrop the oldest user/assistant pair first. Simplest. Risky if turn 1 carries critical state.
- Importance scoringTag each message with high / medium / low. Drop low first, then medium, never high. ~60% token cut while keeping 90%+ of signal.
- Semantic dedupIf the user asked the same thing twice, drop the duplicate exchange. Cheap, surprisingly effective on long support chats.
- Role-based retentionAlways keep tool results and explicit user decisions, regardless of age. These often contain numbers Claude can't recompute.
- Summarize-and-replaceTake a block of low/medium messages, send to Claude with "summarize, keep IDs verbatim", replace the block with the summary. Pair with strategy 2 for best results.
After every prune, verify user/assistant alternation is intact — remove a user message and the orphaned assistant reply must go too, or the next API call fails.
Pseudocode
# Each message carries metadata SCHEMA Message: role: "user" | "assistant" content: text importance: "high" | "medium" | "low" tags: ["account_id", "tool_result", ...] FUNCTION prune(messages, target_tokens): # 1. drop low-importance pairs (oldest first) WHILE tokens(messages) > target_tokens: pair = oldest_pair_with(messages, importance="low") IF pair: messages.remove(pair); CONTINUE # 2. summarize medium-importance block if still over medium = oldest_block_with(messages, importance="medium") IF medium: summary = CALL Claude("summarize, keep tags verbatim", medium) messages = replace(messages, medium, [summary_pair(summary)]) CONTINUE # 3. NEVER drop "high" -- raise instead RAISE ContextOverflow("can't trim further") assert valid_alternation(messages) RETURN messages
Importance is set on insert. The cheapest signals work: tag tool results "high", greetings "low", everything else "medium".
Misconceptions
Pruning is information density preservation. Drop noise (greetings, dedup'd questions), summarize medium blocks, and never touch tagged high-importance facts.
Real-world impact: a 20-message support chat goes from ~3,000 tokens to ~1,200 (60% reduction) while retaining ~91% of the actionable information.
One class to rule the chat
A production conversation manager is a single class that sits between your application and the Claude API. It owns the messages array, tracks tokens, triggers pruning, summarizes when needed, attaches metadata, and serializes state to disk so conversations survive a server restart.
Without one, every endpoint in your codebase rolls its own message-handling logic — and every endpoint hits the same bugs: phantom user turns, broken alternation, runaway token costs, lost account numbers. With one, you write the logic once and every feature inherits it.
The chief of staff
BEFORE: Imagine a CEO who takes every meeting personally, walking in with a growing stack of every note from every past meeting — verbatim, unfiltered, in chronological order.
PAIN: By month three, the stack is so tall the CEO spends more time flipping through old notes than engaging in the current meeting. Decisions from week one are buried under pages of "sounds good" and "let's circle back". Sometimes the CEO misses critical commitments entirely.
MAPPING: A conversation manager is that CEO's chief of staff. It records every exchange, scores messages by importance, compresses old meetings into executive summaries, pins critical decisions to the top, and ensures the CEO walks into every meeting with exactly the right briefing — recent details in full, older context summarized, key facts always on top.
Six responsibilities
- Message storageAppend, retrieve, and edit messages in a single ordered array. The source of truth.
- Token countingTrack per-message and cumulative input/output tokens. Always know how close to the budget you are.
- Automatic pruningTrigger when tokens cross a configurable threshold (e.g., 100K). No manual babysitting from app code.
- SummarizationUse Claude itself to compress old turns into a short paragraph — with explicit instructions to keep IDs and names verbatim.
- Metadata trackingTimestamps, token counts, importance flags, tool-result tags. The fuel for smart pruning.
- SerializationSave and load full state to JSON. Conversations survive server restarts, deploys, and user device switches.
Real impact: a healthcare pre-auth agent at 500 cases/day drops from $34/day to $12/day on input tokens (65% cut) when wrapped by a tuned manager — while keeping 95%+ accuracy on clinical references.
Pseudocode
CLASS SmartConversationManager: system_prompt messages = [] token_threshold = 100_000 recent_to_keep = 4 pinned_facts = [] # never summarized METHOD send(user_text, importance="medium"): self.messages.append({user, user_text, importance}) IF tokens(self.messages) > self.token_threshold: self.compress() TRY: ctx = self.pinned_facts + self.messages reply = CALL Claude(self.system_prompt, ctx) self.messages.append({assistant, reply}) RETURN reply CATCH error: self.messages.pop() # keep alternation valid RAISE METHOD compress(): old = self.messages[:-self.recent_to_keep] keep = self.messages[-self.recent_to_keep:] summary = CALL Claude("summarize, keep IDs verbatim", old) self.messages = [ {user, "[SUMMARY] " + summary, importance="medium"}, {assistant, "Got it."} ] + keep METHOD save(path): write_json(path, self.__dict__) METHOD load(path): self.__dict__ = read_json(path)
The full desktop module ships three working classes — basic, sliding window, and smart — in Python and Node.js, with token counting and JSON persistence built in.
Misconceptions
The conversation manager is the unsung hero of every production agent. One class encapsulates statelessness, history pattern, token budget, pruning, and persistence.
Cert tip (Domain 5.1): position immutable "case facts" at the START of context — the high-recall position — and never summarize them. Names, IDs, amounts, and dates belong there.
One question per concept
Tap a card to reveal the answer.
messages array, it doesn't exist as far as Claude knows. The whole "memory" experience is your code replaying past turns on every request.max_tokens for the model's reply — that space is not free, even though nothing has been generated yet.Open the full module on desktop
The desktop version walks through three working ConversationManager classes — full history, sliding window, smart-with-summarization — with token-budget math, error recovery, persistence to JSON, and benchmark comparisons in Python and Node.js.
Open M08 on Desktop → Full code · Hands-on lab · ~55 minModule 8 of 30 · Track 3: Memory & Context