Understanding stop_reason in the Claude Messages API

June 23, 2026 · updated June 25, 2026 · How Claude Actually Works (part 5)

▶ 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_reason is 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.

ValueWhat it meansWhat you must do
end_turnModel finished cleanlyReturn content to the user. Loop ends.
tool_useModel wants to call one or more toolsExecute the tools, append the result, call the API again.
max_tokensOutput was cut mid-generation by your max_tokens budgetContinue the turn — append the partial response and call again.
stop_sequenceOne of your custom stop tokens was hitHandle as a planned, clean terminator.
pause_turnThe server-side iteration ceiling was reachedNot an error. Resume by appending the response and calling again.
refusalModel declined on safety groundsDo not retry blindly. Surface it gracefully.
model_context_window_exceededTotal input tokens hit the model’s context limitPrune, 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:

  1. Execute whatever the model asked for.
  2. Append the assistant’s full response to messages.
  3. Append a user message containing tool_result blocks keyed to the same id values.
  4. Call messages.create again.

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:

CheckpointQuestion to ask
Branch exhaustivenessDoes every stop_reason value have an explicit case, plus a default?
tool_use handlingDo you append the assistant response and the tool_result before calling again?
max_tokens continuationDo you continue the turn (not retry from scratch) on max_tokens?
pause_turn resumptionDo you resume (not fail) on pause_turn?
context managementDo you have a prune/compact strategy for model_context_window_exceeded?
refusal surfacingDo you surface refusals to the user rather than retrying in a loop?
forward compatibilityIs 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_reason is. Prose is for users; enum values are for code. The same output text might appear on both an end_turn and a pause_turn response.

  • pause_turn means 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_tokens and model_context_window_exceeded are the same thing.” They are not. max_tokens fires when your output budget runs out mid-generation. model_context_window_exceeded fires when the total context — input plus output — exceeds the model’s window. They require different fixes: continue the turn for max_tokens; prune the conversation history for model_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 →