CLAUDE LABJP
FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as OctoberFORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Articles/API & SDK
API & SDK/2026-04-26Intermediate

Reading Claude API stop_reason Correctly — A Production Guide to end_turn, max_tokens, pause_turn, and refusal

Branching on Claude API's stop_reason properly eliminates a surprising number of production incidents — truncated outputs, missed tool continuations, wasted retries. Here is how to tell end_turn, max_tokens, pause_turn, and refusal apart.

Claude46API27stop_reason2error-handling11production111

"Claude's response feels like it's getting cut off mid-sentence." If you've shipped anything on the Messages API, you've likely heard this from a user. The first time I deployed a Claude-powered summarizer to production, I wasn't paying attention to stop_reason — and a max_tokens-truncated meeting summary went straight to a customer with the final decision missing. The bug wasn't in my prompt. It was in not reading the one field that tells you why the model stopped.

stop_reason is a small response field, but branching on it correctly prevents a whole category of production accidents — truncated outputs delivered as if complete, tool calls that silently halt the conversation, retries that achieve nothing. This guide walks through all six values and the practical patterns I use to handle each one without overengineering.

stop_reason is the only signal that tells you how the response ended

Every Messages API response carries a stop_reason. The six common values are:

  • end_turn: The model ended its turn naturally. The healthy case.
  • max_tokens: Generation hit the max_tokens ceiling. The output is almost certainly incomplete.
  • stop_sequence: One of the stop_sequences you supplied appeared in the output.
  • tool_use: The response contains tool-use blocks. You must execute the tools and continue the conversation.
  • pause_turn: Extended thinking or a long-running server-side tool paused the turn. You need to call again to resume.
  • refusal: The model declined to respond on safety grounds.

The trap most teams fall into is treating "I got a stop_reason back" as "the response is complete." It isn't. Three of the six — max_tokens, tool_use, pause_turn — mean the model stopped mid-task. Without a follow-up call, the user gets a partial answer.

Confusing end_turn with max_tokens is the most common production bug

The accident I see most often is shipping a max_tokens-truncated response as if the turn ended cleanly. Consider this minimal implementation:

# A naive Messages API call — not safe for production
import anthropic
 
client = anthropic.Anthropic()
 
resp = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Summarize this long meeting transcript..."}],
)
 
# ⚠️ Returns the body without checking stop_reason
print(resp.content[0].text)

If the response hit the token limit, this code happily renders a paragraph that ends mid-word. In my own incident, the truncation happened during the "decisions made" section of a meeting summary — the part the customer cared about most. Branching on stop_reason is the cheapest fix for the highest-impact failure mode:

# Minimal stop_reason branching
resp = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[{"role": "user", "content": "..."}],
)
 
text = "".join(b.text for b in resp.content if b.type == "text")
 
if resp.stop_reason == "end_turn":
    return {"status": "ok", "text": text}
elif resp.stop_reason == "max_tokens":
    # Either continue or retry with a higher max_tokens
    return {"status": "truncated", "text": text, "hint": "raise max_tokens"}
elif resp.stop_reason == "refusal":
    return {"status": "refused", "text": text}
else:
    return {"status": resp.stop_reason, "text": text}
# Example output (truncated case):
# {"status": "truncated", "text": "...partial summary...", "hint": "raise max_tokens"}

A note on the fix: don't reflexively double max_tokens and retry. Ask first whether the original ceiling was even reasonable for the task. Summarization rarely fits in 1,024 tokens; multi-step reasoning routinely exceeds 2,000. Setting a sane default like 4,096–8,192 makes most max_tokens truncations disappear without any retry logic at all. Reserve aggressive retry-with-larger-budget logic for tasks where you genuinely cannot estimate output length up front, like document-to-document translation.

tool_use and pause_turn both mean "keep going"

When tool_use comes back, the response contains one or more tool_use blocks that you need to execute. Their results go back as a new user message, and the conversation continues. Skip this loop and the user sees a model that "always stops as soon as it tries to use a tool."

# The standard tool_use continuation loop
def run_with_tools(messages, tools):
    resp = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=4096,
        tools=tools,
        messages=messages,
    )
 
    while resp.stop_reason == "tool_use":
        tool_blocks = [b for b in resp.content if b.type == "tool_use"]
        tool_results = []
        for block in tool_blocks:
            output = run_tool(block.name, block.input)  # your code
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": str(output),
            })
 
        messages.append({"role": "assistant", "content": resp.content})
        messages.append({"role": "user", "content": tool_results})
 
        resp = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=4096,
            tools=tools,
            messages=messages,
        )
 
    return resp

pause_turn is the cousin of tool_use for cases like extended thinking or long-running server-side tools (code execution). The model is essentially saying "this turn isn't over, please call me again." The continuation pattern is even simpler: append the assistant message and call messages.create once more — no extra user message required.

For the streaming-plus-tool variant, see Combining Streaming and Tool Use in the Claude API, which goes into the event ordering you'll need.

refusal is a design decision, not a retry candidate

refusal means the model declined for safety reasons. The first instinct of many teams — including mine, the first time — is to retry. Resending the same prompt achieves nothing except burning rate limit. Three rules I now follow without exception:

  • Don't auto-retry the same prompt. The model's safety classifier is essentially deterministic for a given input, so retries cost rate limit and produce the same outcome.
  • Show the user a neutral "we can't respond to this" message. Don't surface the model's refusal text directly.
  • Log the input for human review. Sometimes the refusal is correct; sometimes the prompt was unintentionally triggering it and is worth refining.

Pair this with the retry policy in Claude API Error Handling so that 429s, 5xx errors, and refusal aren't treated identically.

In streaming, stop_reason arrives last

Streaming surfaces stop_reason inside a message_delta event near the end of the stream — not in message_start. If you try to read it at stream open, you'll always see null.

# Capturing stop_reason in a streamed response
stop_reason = None
with client.messages.stream(
    model="claude-sonnet-4-6",
    max_tokens=4096,
    messages=[{"role": "user", "content": "..."}],
) as stream:
    for event in stream:
        if event.type == "message_delta" and event.delta.stop_reason:
            stop_reason = event.delta.stop_reason
        # text_delta handling omitted
 
print("stop_reason:", stop_reason)
# Example: stop_reason: end_turn

A subtlety on the UI side: if your "thinking" indicator hides only on end_turn, a pause_turn mid-stream can leave the spinner running while the conversation actually needs another API call. Treat any of tool_use / pause_turn / max_tokens as "still in progress" and you'll avoid that class of frozen-UI bug. The same logic applies to your timeout watchdog — if your code aborts a stream after N seconds of silence, make sure that watchdog resets when you trigger the continuation call, not when the original stream closes.

stop_sequence is useful, but easy to forget you set it

stop_sequence only appears when one of the strings in stop_sequences shows up in the model's output. It's a powerful tool for forcing structured generation — for example, you can stop generation as soon as the model emits a closing tag like </answer> and trim the response cleanly. The pattern looks like this:

# Use stop_sequences to constrain output
resp = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=2048,
    stop_sequences=["</answer>"],
    messages=[{"role": "user", "content": "Wrap your answer in <answer>...</answer>"}],
)
 
if resp.stop_reason == "stop_sequence":
    # resp.stop_sequence tells you which one fired
    print("matched:", resp.stop_sequence)

The pitfall I keep seeing in code reviews: someone sets stop_sequences=["\n\n"] to make answers terse, then forgets about it months later. New tickets come in saying "Claude won't produce multi-paragraph output anymore." The fix is to grep your codebase for stop_sequences whenever you see unexplained short responses — old configuration is the most common culprit.

A quick checklist for handling each value

To make this concrete, here is the minimal correct behavior I encode for each stop_reason:

  • end_turn — return the response. Done.
  • max_tokens — log it, return a "truncated" status, surface the truncation in your error tracker. If the use case demands a full answer, raise max_tokens rather than retrying blindly.
  • stop_sequence — strip the matched sequence if needed, then return. Verify your stop_sequences configuration is still relevant.
  • tool_use — execute the tool blocks, append assistant + user messages, call again. Cap the loop iterations to avoid runaway sessions.
  • pause_turn — append the assistant message and call again. No extra user message required.
  • refusal — show a neutral message to the user, log the input for review, do not auto-retry.

Start by catching max_tokens — the rest can wait

You don't need to handle all six values perfectly on day one. The highest-leverage move is to log every max_tokens response and surface it to your error tracker today. That single change makes most "the response cuts off" reports disappear, because they were never a model problem to begin with — they were a missing branch in your code. The tool_use, pause_turn, and refusal paths can grow with your product surface area.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

API & SDK2026-06-22
Putting a Ceiling on the pause_turn Loop: Running Long Server Tools Safely Unattended
A production design for continuing pause_turn safely in unattended runs, where long server tools like web_search and code execution are involved. Covers branching all four stop_reason values in one loop, capping continuations and wall-clock time, and accumulating usage across paused segments.
API & SDK2026-04-23
Production Prompt-Injection Defense for the Claude API — Detection, Sanitization, and Layered Guardrails
A practical, code-first design guide for defending Claude API applications against prompt injection — covering input sanitization, channel separation, output validation, and red-teaming for long-term safety.
API & SDK2026-04-08
Claude API Webhooks & Async Processing: Error Patterns and Recovery Strategies
A practical guide to handling errors when integrating Claude API with webhooks and async pipelines. Covers timeouts, duplicate processing, idempotency, dead-letter queues, circuit breakers, and graceful degradation with full Python examples.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →