BeClaude
Guide2026-04-28

Mastering Claude’s Stop Reasons: Build Reliable API Applications

Learn how to interpret and handle Claude API stop_reason values—end_turn, tool_use, max_tokens, and stop_sequence—with practical code examples and troubleshooting tips.

Quick Answer

This guide explains Claude’s stop_reason field, covering end_turn, tool_use, max_tokens, and stop_sequence. You’ll learn how to detect empty responses, handle tool calls, and implement retry logic for robust applications.

Claude APIstop_reasontool useerror handlingAPI best practices

Introduction

When you call the Claude API, every successful response includes a stop_reason field. This tiny piece of data tells you why Claude stopped generating—whether it finished naturally, requested a tool call, hit a token limit, or encountered a custom stop sequence. Understanding and handling these reasons correctly is essential for building reliable, production-ready applications.

In this guide, you’ll learn:

  • What each stop_reason value means
  • How to handle tool calls by detecting tool_use
  • How to prevent and recover from empty responses
  • How to implement retry logic for max_tokens and stop_sequence
Let’s dive in.

The stop_reason Field

Every successful response from the Messages API includes a stop_reason field. Here’s a typical example:

{
  "id": "msg_01234",
  "type": "message",
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "Here's the answer to your question..."
    }
  ],
  "stop_reason": "end_turn",
  "stop_sequence": null,
  "usage": {
    "input_tokens": 100,
    "output_tokens": 50
  }
}

Unlike API errors (which indicate a failure), stop_reason tells you why Claude successfully completed its response generation. There are four possible values:

ValueMeaning
end_turnClaude finished naturally and expects no further input
tool_useClaude wants to call a tool (function)
max_tokensClaude stopped because it hit the max_tokens limit
stop_sequenceClaude encountered a custom stop sequence you defined

Handling end_turn

end_turn is the most common stop reason. It means Claude has completed its response and doesn’t expect any follow-up. In most cases, you can simply display the response to the user.
from anthropic import Anthropic

client = Anthropic() response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=[{"role": "user", "content": "Hello!"}] )

if response.stop_reason == "end_turn": print(response.content[0].text)

The Empty Response Problem

Sometimes Claude returns an empty response (2–3 tokens with no content) with stop_reason: "end_turn". This typically happens in tool-use workflows when:

  • You add text blocks immediately after tool_result blocks
  • You send back Claude’s completed response without adding anything new
Why it happens: Claude learns patterns from the conversation history. If you repeatedly insert text after tool results, Claude learns to expect that pattern and may end its turn prematurely. How to prevent it:
# INCORRECT: Adding text immediately after tool_result
messages = [
    {"role": "user", "content": "Calculate the sum of 1234 and 5678"},
    {"role": "assistant", "content": [{
        "type": "tool_use",
        "id": "toolu_123",
        "name": "calculator",
        "input": {"operation": "add", "a": 1234, "b": 5678}
    }]},
    {"role": "user", "content": [
        {"type": "tool_result", "tool_use_id": "toolu_123", "content": "6912"},
        {"type": "text", "text": "Here's the result"}  # Don't do this!
    ]}
]

CORRECT: Send tool results directly without additional text

messages = [ {"role": "user", "content": "Calculate the sum of 1234 and 5678"}, {"role": "assistant", "content": [{ "type": "tool_use", "id": "toolu_123", "name": "calculator", "input": {"operation": "add", "a": 1234, "b": 5678} }]}, {"role": "user", "content": [ {"type": "tool_result", "tool_use_id": "toolu_123", "content": "6912"} ]} # Just the tool_result, no extra text ]

If you still get empty responses after fixing the message structure, implement a retry loop:

def handle_empty_response(client, messages):
    response = client.messages.create(
        model="claude-opus-4-7",
        max_tokens=1024,
        messages=messages
    )
    
    if response.stop_reason == "end_turn" and not response.content:
        # Retry with a prompt to continue
        messages.append({"role": "assistant", "content": ""})
        messages.append({"role": "user", "content": "Please continue."})
        return client.messages.create(
            model="claude-opus-4-7",
            max_tokens=1024,
            messages=messages
        )
    return response

Handling tool_use

When Claude decides it needs to call a tool (function), it sets stop_reason to tool_use. Your application must detect this, execute the tool, and return the result.

response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=[
        {
            "name": "get_weather",
            "description": "Get current weather for a city",
            "input_schema": {
                "type": "object",
                "properties": {
                    "city": {"type": "string"}
                },
                "required": ["city"]
            }
        }
    ],
    messages=[{"role": "user", "content": "What's the weather in Paris?"}]
)

if response.stop_reason == "tool_use": for block in response.content: if block.type == "tool_use": tool_name = block.name tool_input = block.input tool_id = block.id # Execute the tool (your implementation) result = execute_tool(tool_name, tool_input) # Append tool result and continue the conversation messages = [ {"role": "user", "content": "What's the weather in Paris?"}, {"role": "assistant", "content": response.content}, {"role": "user", "content": [{ "type": "tool_result", "tool_use_id": tool_id, "content": str(result) }]} ] # Continue the conversation final_response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, tools=[...], messages=messages )

Important: After processing a tool call, always check the new stop_reason. Claude may call multiple tools in sequence or return end_turn after receiving results.

Handling max_tokens

When Claude hits the max_tokens limit, the response is truncated. This is common for long-form content. You should detect this and either:

  • Increase max_tokens for the next request
  • Continue the conversation by sending Claude’s response back and asking it to continue
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=200,  # Deliberately low for demonstration
    messages=[{"role": "user", "content": "Write a long story about a robot."}]
)

if response.stop_reason == "max_tokens": # Continue the conversation messages = [ {"role": "user", "content": "Write a long story about a robot."}, {"role": "assistant", "content": response.content}, {"role": "user", "content": "Please continue from where you left off."} ] continuation = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=2000, # Increase limit messages=messages )

Handling stop_sequence

If you define custom stop sequences (e.g., ["END"]), Claude will stop when it generates that sequence. The stop_sequence field in the response tells you which sequence was encountered.

response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    stop_sequences=["END"],
    messages=[{"role": "user", "content": "List three colors and then write END."}]
)

if response.stop_reason == "stop_sequence": print(f"Stopped at sequence: {response.stop_sequence}") # Remove the stop sequence from the output if needed clean_text = response.content[0].text.replace("END", "").strip()

Building a Robust Handler

For production applications, combine all checks into a single handler:

def handle_claude_response(client, messages, tools=None):
    while True:
        response = client.messages.create(
            model="claude-sonnet-4-20250514",
            max_tokens=4096,
            tools=tools,
            messages=messages
        )
        
        if response.stop_reason == "end_turn":
            if response.content:
                return response.content[0].text
            else:
                # Empty response - retry
                messages.append({"role": "user", "content": "Please continue."})
                continue
        
        elif response.stop_reason == "tool_use":
            for block in response.content:
                if block.type == "tool_use":
                    result = execute_tool(block.name, block.input)
                    messages.append({"role": "assistant", "content": response.content})
                    messages.append({"role": "user", "content": [{
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": str(result)
                    }]})
        
        elif response.stop_reason == "max_tokens":
            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": "Please continue."})
        
        elif response.stop_reason == "stop_sequence":
            clean_text = response.content[0].text.replace(response.stop_sequence, "").strip()
            return clean_text

Key Takeaways

  • end_turn is the normal completion signal; watch for empty responses in tool workflows and fix by removing extra text after tool_result blocks.
  • tool_use requires you to execute the tool and return results; always check for multiple tool calls in a single response.
  • max_tokens means the response was truncated; continue the conversation with a higher limit or ask Claude to finish.
  • stop_sequence indicates a custom stop was triggered; clean the output if needed.
  • Build a loop that handles all stop reasons to create resilient, production-ready Claude applications.