Conversations and memory
APIs are stateless: how chat history and memory really work.
Create a free account Try part of it here
Why it matters
Chat is the most familiar shape for an AI product. It rests on a fact that surprises most people: the model remembers nothing between requests. The memory you feel in a chat app is code that resends the whole conversation on every turn. Once you manage that history yourself, you decide what the model sees, what each turn costs, and what your app remembers about a person. That's the base for any multi-turn feature, from a copy assistant to an agent.
What you'll be able to do
- Keep a conversation going by appending each turn to a history list and resending it
- Explain why long conversations get slower and more expensive, and trim history to a budget
- Tell conversation history apart from long-term memory and from business data
- Build a small chat helper that manages its history
What it covers
- The model has no memory between calls
- Worked example: a chat helper
- What to store for the assistant's turn
- Keeping history in check
- Worked example: a sliding window
- Memory is a separate thing
The model has no memory between calls
Picture texting a friend who forgets everything the moment they reply. To keep the chat going, you'd have to paste the whole thread into every new message. That's how a model works.
Every request to the API stands alone. The model reads what's in that one request and writes a reply. Then it keeps nothing. A follow-up like "Make it shorter" means nothing on its own. The model can't tell what "it" refers to.
The word for this is stateless: nothing carries over from one request to the next.
Chat apps fake a continuous conversation with a list. Your code keeps every turn in the list, and each request sends all of it:
- The user types a message. Append it to the history as a
"user"message. - Send the system prompt and the entire history.
- Append the model's reply to the history as an
"assistant"message. - Repeat for the next message.
The model reads the conversation fresh every time, like a transcript. That's why it can refer back to something said ten turns ago. It's also why it can't refer to anything you left out.
Try it in the request builder below.
history = [] for turn in ["Hi", "And?"]: history.append(turn) sent = len(history) print(sent)
Show the answer
2
Each pass adds one turn, so history has 1 item and then 2, and sent ends at 2. The model remembers nothing, so each call sends the whole conversation so far.
Send it. Click "Add a turn" and paste the reply into the new assistant message. Add a user turn saying "Make it shorter", and send again. Then clear the first two boxes, so only "Make it shorter" is sent. Send once more and compare.
messages?Show the answer
5, the whole history including the new message
Each request sends the entire history. Two exchanges make four messages, and the new user message makes five. The reply doesn't exist yet: it gets appended after it comes back.
Worked example: a chat helper
This function sends one user message as part of an ongoing conversation. The history lives in a list outside the function, so it survives between calls. Read the code, then run it.
import requests SYSTEM = "You help a marketing team write email copy for Trailhead Outfitters. Keep replies short." history = [] def chat(user_text): history.append({"role": "user", "content": user_text}) response = requests.post( "https://api.anthropic.com/v1/messages", headers={"x-api-key": "added-by-the-academy-server", "anthropic-version": "2026-10-21"}, json={"model": "claude-opus-5-5", "max_tokens": 1000, "system": SYSTEM, "messages": history}, timeout=60, ) response.raise_for_status() reply = "" for block in response.json()["content"]: if block["type"] == "text": reply += block["text"] history.append({"role": "assistant", "content": reply}) return reply print(chat("Write a subject line for our spring rain jacket sale.")) print() print(chat("Make it shorter.")) print() for message in history: print(message["role"], "|", message["content"][:50])
After two calls, history holds four messages. They alternate user and assistant. The second request carried three of them. The simulated reply reports the message count, so you can watch the conversation grow.
The system prompt isn't part of history. It goes in its own field on every request.
One gap a real app would close: if a request fails, the user message stays in history with no reply after it. Before the next turn, you'd remove it or retry.
What to store for the assistant's turn
The helper above stores the reply's text. That's all a plain conversation needs, and it's what you'll do in this lesson.
Go deeper Storing the full reply, thinking included
Some replies contain thinking blocks: the model's written reasoning before its answer. You can store the response's whole content list, thinking blocks included, as the assistant message: {"role": "assistant", "content": data["content"]}. That lets the model see its own earlier reasoning. You'll need it once the model starts using tools.
It comes with a rule. Current Claude models check that nothing before a returned thinking block has changed. They can reject a request that sends one back after earlier messages or the system prompt were edited. So histories that keep thinking blocks should only ever be appended to. Text-only histories, like the ones you build here, can be trimmed and rewritten freely.
Long conversations get slower and cost more
Think back to the friend who needs the whole thread pasted in. Every new message makes the paste longer. Requests work the same way. Each one resends the whole conversation as input tokens. (A token is a word or a piece of a word, and you pay for every one you send.)
Here's the size of one request, in words:
In plain words: tokens in this request = tokens per earlier turn × number of earlier turns + tokens in the new message.
Work out turn 3 with small numbers. Say each turn (one user message plus one reply) is about 300 tokens. The new message is about 100 tokens. Turn 3 has two earlier turns:
- earlier turns: 300 × 2 = 600 tokens
- new message: 100 tokens
- this request: 600 + 100 = 700 tokens
Turn 1 sent only 100 tokens. Turn 3 sends 700. The code below runs the same sum for 20 turns and keeps a running total:
tokens_per_turn = 300 # one user message plus one reply, roughly total = 0 for turn in range(1, 21): this_request = tokens_per_turn * (turn - 1) + 100 # all earlier turns, plus the new message total += this_request if turn in (1, 5, 10, 20): print(f"Turn {turn}: {this_request} input tokens in this request, {total} sent so far")
By turn 20, one request is 5,800 tokens. Twenty short turns send 59,000 input tokens in total. Yet the conversation itself is only about 6,000 tokens long. Each turn costs more than the last, so the total climbs much faster than the number of turns.
Long histories also take longer to process. A very long one can eventually pass the model's context window, the most text it can handle in one request.
Show the answer
1,000
Turn 4 has three earlier turns: 300 × 3 = 900. Add the 100-token new message and you get 1,000.
Keeping history in check
You have four common options. Each trades cost against what the model gets to see.
- Keep the last N messages. N is a number you pick, such as 10. This is called a sliding window, like a group chat that only shows the latest messages. It's simple and cheap, but early details vanish. Examples: the product the user named in their first message, or a decision made an hour ago.
- Summarize older turns. Ask the model for a short summary of the early conversation, like meeting notes. Then send that summary plus the recent turns. Key facts survive. The cost is an extra call and some lost detail.
- Pin the facts that matter. Keep a short list of decisions and details in code: the campaign, the audience, the agreed tone. Put it in the system prompt on every request.
- Let the API do it. Anthropic's API offers server-side compaction. It summarizes older context automatically once a conversation gets long. It was a beta feature at the time of writing.
Worked example: a sliding window
Keep only the most recent messages. There's one catch: the first message you send must come from the user.
def trim_history(history, max_messages): trimmed = history[-max_messages:] if trimmed and trimmed[0]["role"] == "assistant": trimmed = trimmed[1:] return trimmed history = [ {"role": "user", "content": "Write a subject line for the rain jacket sale."}, {"role": "assistant", "content": "Rain jackets 30% off, through Sunday"}, {"role": "user", "content": "Make it shorter."}, {"role": "assistant", "content": "30% off rain jackets"}, {"role": "user", "content": "Now one for the fleece launch."}, {"role": "assistant", "content": "Meet the Ridgeline Fleece"}, {"role": "user", "content": "Add the price."}, ] recent = trim_history(history, 4) print(len(history), "messages in the full history,", len(recent), "sent") for message in recent: print(message["role"], "|", message["content"])
history[-4:] takes the last four messages. The minus sign counts from the end of the list. Here those four start with an assistant reply. The first message sent must have the "user" role, so the function drops that reply and sends three.
Slicing makes a new list, so the full history is untouched. Your app can still show the whole conversation on screen while it sends only the recent part.
Notice what the model loses. "Add the price" is clear in context. But the rain jacket conversation is gone.
Show the answer
The first message in messages must use the user role, or the API rejects the request
The API requires messages to start with a user message. A sliding window can cut a conversation in the middle of an exchange. So check the first role after trimming.
Memory is a separate thing
Think of your phone. The thread with a friend is one thing. The note you saved on their contact card ("allergic to nuts") is another. The note is still there when you start a new thread.
Apps work the same way:
- Conversation history is the transcript of one chat.
- Long-term memory is facts saved outside any single chat, in a database or a file. The app adds them to future requests.
When an app "remembers" that you like short drafts, it stored a note. Later, it put that note in the system prompt of a new conversation. The model itself learned nothing.
You help a marketing team write email copy for Trailhead Outfitters. <user_notes> - Prefers short drafts, no emoji. - Works on the email newsletter, not paid social. </user_notes>
Memory is data you keep about a person. Design it like any other customer data:
- Store what's useful, for a clear purpose.
- Tell users what's remembered.
- Let them edit or delete it.
- Never store secrets such as passwords or card numbers.
Business data isn't memory either. Budgets, order statuses, stock levels and campaign results live in your real systems. They can change at any time. Say a user mentioned "a $5,000 budget" thirty turns ago, and finance has since cut it. The conversation is now out of date. When you need the figure, fetch it from the system that owns it. Put that current figure in the request.
Show the answer
The current $4,000 figure, fetched from the budgeting system for this request
Conversation history is a snapshot of what was said. For facts that change, ask the system that owns them each time.
Practice what you just read
- 3 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 5 review cards for spaced repetition
Try a quiz question
A user types "Make it shorter." Your app sends only that message to the API, with no history. What happens?