Understanding stop_reason in the Claude Messages API
▶ Watch on YouTube & subscribe to The Stack Underflow
Every agent, every Claude Code action, every multi-step AI workflow ever built runs on a single function call made in a loop. That call is messages.create. It returns two things — content and stop_reason. Most developers pay close attention to content. The ones who build reliable, production-grade agents pay equally close attention to stop_reason, because that field is the entire control plane of your agent loop.
Getting it wrong is not a subtle bug. It is the kind that causes your agent to silently abandon tasks mid-flight, loop forever, or surface a refusal as a success. Understanding it precisely is a prerequisite for everything else in this series.
The one-sentence version:
stop_reasonis the enum that tells you why Claude stopped generating — and every branching decision in your agent loop must be driven by it, never by reading the text content.
The protocol in one diagram
The messages.create call is Layer 1 of the Claude stack — the foundation everything else rests on. You send it a model, a messages array, and optionally a list of tools. Back comes a response object. Two fields matter for control flow:
┌──────────────────────────────────────────────┐
│ messages.create(...) │
│ │
│ IN: model, messages[], tools[]? │
│ │
│ OUT: content[] stop_reason │
│ ├── text blocks "end_turn" │
│ └── tool_use "tool_use" │
│ blocks "max_tokens" │
│ "stop_sequence" │
│ "pause_turn" │
│ "refusal" │
│ "model_context_ │
│ window_exceeded" │
└──────────────────────────────────────────────┘
content is an array that can hold text blocks, tool_use blocks, or both. stop_reason is a string enum telling you why the model stopped producing output. That is the whole protocol at layer one. Everything an agent framework does is a structured loop around this single call.
Why you must never branch on content text
Here is the classic anti-pattern that ships to production every week:
response = client.messages.create(
model="claude-sonnet-4-6",
messages=messages
)
text = response.content[0].text
if "all done" in text.lower():
mark_task_complete() # this will bite you
Checking prose for completion signals is fragile. The model might phrase the finish differently, use a different language, add a summary paragraph that looks like a terminator but is not, or return a tool_use block before any text at all. Branch on stop_reason. Always.
response = client.messages.create(
model="claude-sonnet-4-6",
messages=messages,
tools=tools
)
match response.stop_reason:
case "end_turn":
return response.content # clean finish
case "tool_use":
results = run_tools(response.content)
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": results})
# call messages.create again
case "max_tokens":
messages.append({"role": "assistant", "content": response.content})
# continue the turn — output was cut mid-thought
case "model_context_window_exceeded":
# input + output together hit the context limit
# prune, summarise, or compact before retrying
raise ContextWindowError(response)
case "stop_sequence":
return response.content # your planned terminator fired
case "pause_turn":
messages.append({"role": "assistant", "content": response.content})
# resume — server hit its iteration ceiling, not an error
case "refusal":
return surface_refusal_to_user(response)
case _:
raise UnknownStopReasonError(response.stop_reason)
All seven stop_reason values
As of mid-2026, the Messages API defines seven documented stop_reason values. The first four have existed since the API launched. pause_turn and refusal were formalised later. model_context_window_exceeded shipped in August 2025 (docs.anthropic.com, 2025) and became stable on Sonnet 4.5 and newer without a beta header.
| Value | What it means | What you must do |
|---|---|---|
end_turn | Model finished cleanly | Return content to the user. Loop ends. |
tool_use | Model wants to call one or more tools | Execute the tools, append the result, call the API again. |
max_tokens | Output was cut mid-generation by your max_tokens budget | Continue the turn — append the partial response and call again. |
stop_sequence | One of your custom stop tokens was hit | Handle as a planned, clean terminator. |
pause_turn | The server-side iteration ceiling was reached | Not an error. Resume by appending the response and calling again. |
refusal | Model declined on safety grounds | Do not retry blindly. Surface it gracefully. |
model_context_window_exceeded | Total input tokens hit the model’s context limit | Prune, summarise, or compact the conversation before retrying. |
end_turn
The happy path. The model finished what it was saying and handed control back to you. Return content to the user and exit the loop. No further action needed.
tool_use
The most important value for agent builders. When you see tool_use, the content array will contain one or more tool_use blocks, each with a name, an id, and input. Your job:
- Execute whatever the model asked for.
- Append the assistant’s full response to
messages. - Append a
usermessage containingtool_resultblocks keyed to the sameidvalues. - Call
messages.createagain.
That loop — call → tool → append → call — is the seed from which every Claude agent is grown:
messages.create
│
▼
stop_reason == "tool_use"?
│
├─ yes → run tools
│ │
│ ▼
│ append assistant response
│ │
│ ▼
│ append tool_result blocks
│ │
│ └──────────────────► messages.create (again)
│
└─ no → handle other stop_reason
max_tokens
The model ran out of the output budget you specified in max_tokens mid-generation. The response content is incomplete — do not treat it as a finished answer. Continue the turn by appending the partial assistant response and calling the API again with no new user message. The model picks up where it left off. This stop reason is about your output budget, not the context limit.
model_context_window_exceeded
Different from max_tokens. This fires when the total token count — your input messages, any system prompt, any tool definitions, plus the generated output — has hit the model’s context window ceiling. The current ceilings are 200k tokens for claude-sonnet-4-6 and claude-opus-4-8 (docs.anthropic.com, 2025). Continuing the turn without action will fail again. You must prune old messages, compress history, or use a context-engineering strategy. See Context Engineering: Pin, Summarize, Prune, Compact for the practical techniques.
stop_sequence
You asked for this. A stop sequence is a token string you register with the API as an output terminator. When the model generates it, output cuts there and the field reads stop_sequence. Handle it as a planned event, not an error — you designed this boundary.
pause_turn
This one surprises developers most. When using Anthropic’s server-side built-in tools (like web_search or web_fetch), the API manages a sampling loop internally. That loop has an iteration ceiling — default 10 iterations per turn (docs.anthropic.com, 2026). If the agent hits that ceiling, the API returns pause_turn instead of continuing indefinitely or throwing an error. The work done so far is real and is returned in the response. This is a checkpoint, not a failure.
Resume by appending the assistant’s response and calling messages.create again. The ceiling resets for the new turn. An agent loop that treats pause_turn as terminal will silently abandon work mid-task — one of the most common production defects in agentic systems.
stop_reason == "pause_turn"?
Yes:
┌──────────────────────────────────────────────┐
│ append assistant response to messages[] │
│ call messages.create again (new turn) │
│ iteration ceiling resets │
└──────────────────────────────────────────────┘
NOT: an error. NOT: retry from scratch.
refusal
The model declined to continue on safety grounds. On claude-sonnet-4-6 and claude-opus-4-8, a refusal may also include a stop_details object naming the specific policy category that triggered it. That category is actionable:
- Some categories indicate the phrasing of the request is the issue — a rephrased prompt may work.
- Others indicate the request category is firmly blocked.
Do not retry a refusal in a blind loop. Read the category. Log it. Surface a clear message to the user.
The default branch you must not skip
Anthropic can and does add new stop_reason values — pause_turn and model_context_window_exceeded are both examples added after the API launched. A switch/match statement with no default branch is a latent crash waiting for Anthropic’s next release. Code that raises a typed UnknownStopReasonError and logs the value gives you something to act on. Code that crashes on an unknown string with an unhandled exception gives you a 3am page.
Handling exhaustiveness:
known values (7) → explicit case branches
future values (unknown) → case _ : raise UnknownStopReasonError(value)
null / missing → you are reading the wrong streaming event
How to apply this in production
A reliable agent loop reads stop_reason first, before accessing content at all. Here is a checklist for any agent you ship:
| Checkpoint | Question to ask |
|---|---|
| Branch exhaustiveness | Does every stop_reason value have an explicit case, plus a default? |
| tool_use handling | Do you append the assistant response and the tool_result before calling again? |
| max_tokens continuation | Do you continue the turn (not retry from scratch) on max_tokens? |
| pause_turn resumption | Do you resume (not fail) on pause_turn? |
| context management | Do you have a prune/compact strategy for model_context_window_exceeded? |
| refusal surfacing | Do you surface refusals to the user rather than retrying in a loop? |
| forward compatibility | Is there a default branch that handles unknown future values? |
Use claude-sonnet-4-6 as your default model for most production workloads. Use claude-opus-4-8 where reasoning depth is the priority. Pin to a specific model ID string rather than an alias — Anthropic does not silently swap the weights behind a fixed model ID.
Common misconceptions
-
“I can check if the content text says ‘done’ to know the agent finished.” Text output is not a control signal.
stop_reasonis. Prose is for users; enum values are for code. The same output text might appear on both anend_turnand apause_turnresponse. -
“
pause_turnmeans something went wrong.” It does not. It means the server-side sampling loop hit its iteration ceiling and handed control back to you. The work done so far is real. You resume it; you do not retry from scratch. -
“
max_tokensandmodel_context_window_exceededare the same thing.” They are not.max_tokensfires when your output budget runs out mid-generation.model_context_window_exceededfires when the total context — input plus output — exceeds the model’s window. They require different fixes: continue the turn formax_tokens; prune the conversation history formodel_context_window_exceeded. -
“Refusals are transient errors I should retry in a loop.” Retrying blindly wastes quota and can make things worse. A refusal is a deliberate decision. Read the stop_details category if available, decide whether the request category is retryable at all, and surface a clear message to the user if it is not.
Frequently asked questions
What happens if I append a tool_result but forget to call messages.create again? Your agent silently stops. From the user’s perspective the task just hangs or returns nothing. The Messages API is stateless — it does not call you back. The loop only continues when your code issues the next call.
How do I handle max_tokens without duplicating content?
Append the truncated assistant response to messages exactly as it is, then call the API again with no new user message. The model resumes where it left off. Some implementations append a short user message like "Please continue." for clarity, but the assistant response alone is sufficient.
Can stop_reason ever be null?
Not in a completed synchronous response. If you see null you are almost certainly reading a streaming event before the final message_delta has arrived — specifically a content_block_delta event rather than the message_stop event. In the streaming protocol, stop_reason is populated only in the final event.
Is the pause_turn iteration ceiling per turn or per session?
Per turn. A turn is a single messages.create call that the model responds to. After you resume from pause_turn with a new messages.create call, the ceiling resets for that new turn. A session — the full messages array across many turns — has no analogous ceiling.
How do I tell which tools the model called when stop_reason is tool_use?
Filter response.content for blocks where type == "tool_use". Each block has a name, an id, and input. The id is what you must echo back in your tool_result block — it is how the model correlates which result belongs to which call. You can execute multiple tool calls in parallel if the model requests more than one in a single turn.
Should I send model_context_window_exceeded to the user? No. It is an infrastructure signal, not a user-facing message. Log it, invoke your context-pruning strategy, and retry transparently if possible. If pruning cannot recover enough space, surface a friendly message explaining that the conversation needs to be shortened.
Where this fits in the series
This tutorial covers Layer 1 of the Claude stack — the messages.create protocol and the stop_reason field that governs all agent control flow. Understanding stop_reason cold is the prerequisite for everything that follows. The Claude Stack mental model gives the full layered picture of where this primitive sits. How Claude Uses Tools builds the tool_use loop into a complete working pattern. The Claude Agent Loop shows how Claude Code itself runs this same loop at scale. And if context management is where you want to go next, Context Engineering: Pin, Summarize, Prune, Compact covers what to do when model_context_window_exceeded starts showing up in production. Browse all tutorials to follow the full series.
Found this useful? The deep version lives on YouTube — new breakdowns of how AI dev tools actually work, weekly.
Subscribe on YouTube →