BeClaude
GuideBeginnerAgents2026-05-22

Mastering Claude's Stop Reasons: Build Robust API Applications

Learn how to interpret and handle Claude's stop_reason field in the Messages API, including end_turn, max_tokens, tool_use, and stop_sequence, with practical code examples and troubleshooting tips.

Quick Answer

This guide explains Claude's stop_reason field—end_turn, max_tokens, tool_use, and stop_sequence—and shows how to handle each in your API applications to build reliable, production-ready integrations.

Messages APIstop_reasonerror handlingtool useAPI best practices

Introduction

When you send a request to Claude via the Messages API, the response includes a stop_reason field that tells you why the model stopped generating. Understanding this field is essential for building robust applications that handle different response types appropriately.

Unlike HTTP errors (which indicate something went wrong with your request), stop_reason is part of every successful response. It provides insight into Claude's decision-making process and helps you determine the next step in your application logic.

In this guide, you'll learn:

  • The four possible stop_reason values and what they mean
  • How to handle each stop reason in your code
  • How to troubleshoot common issues like empty responses
  • Best practices for production applications

The stop_reason Field

The stop_reason field appears in every successful Messages API response. 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
  }
}

There are four possible values for stop_reason:

ValueMeaning
end_turnClaude finished its response naturally
max_tokensClaude hit the max_tokens limit you set
tool_useClaude wants to use a tool (function calling)
stop_sequenceClaude encountered a custom stop sequence you defined
Let's explore each one in detail.

end_turn: The Natural Stop

end_turn is the most common stop reason. It means Claude has completed its response and has nothing more to say. This is the ideal outcome for simple Q&A interactions.

Handling end_turn

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": # Process the complete response print(response.content[0].text)

Empty Responses with end_turn

Sometimes Claude returns an empty response (2-3 tokens with no content) with stop_reason: "end_turn". This typically happens in tool-use scenarios when Claude interprets that the assistant turn is complete.

Common causes:
  • Adding text blocks immediately after tool results
  • Sending Claude's completed response back without adding anything new
How to prevent empty responses:
# 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 add text after tool_result
    ]}
]

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 additional text ]

If you still get empty responses after fixing the above, use a continuation prompt:

def handle_empty_response(client, messages):
    response = client.messages.create(
        model="claude-opus-4-20250514",
        max_tokens=1024,
        messages=messages
    )
    
    if response.stop_reason == "end_turn" and not response.content:
        # Add a continuation prompt in a NEW user message
        messages.append({
            "role": "user",
            "content": "Please continue with your response."
        })
        return client.messages.create(
            model="claude-opus-4-20250514",
            max_tokens=1024,
            messages=messages
        )
    return response

max_tokens: Hit the Token Limit

When stop_reason is max_tokens, Claude stopped because it reached the max_tokens limit you specified in your request. This is common for long-form content generation.

Handling max_tokens

response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=100,  # Low limit for demonstration
    messages=[{"role": "user", "content": "Write a detailed essay about AI."}]
)

if response.stop_reason == "max_tokens": # The response was truncated. You can continue by sending the response back. messages.append({"role": "assistant", "content": response.content}) messages.append({"role": "user", "content": "Please continue."}) continued_response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1000, messages=messages )

Best practice: Set max_tokens appropriately for your use case. For open-ended generation, use higher values (e.g., 4096 or 8192).

tool_use: Claude Wants to Call a Tool

When stop_reason is tool_use, Claude has decided it needs to use one of the tools you provided. The response will contain one or more tool_use content blocks.

Handling tool_use

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

if response.stop_reason == "tool_use": for block in response.content: if block.type == "tool_use": # Execute the tool and send the result back tool_name = block.name tool_input = block.input tool_use_id = block.id # Your tool execution logic here result = execute_tool(tool_name, tool_input) # Append tool result to messages messages.append({"role": "assistant", "content": response.content}) messages.append({"role": "user", "content": [{ "type": "tool_result", "tool_use_id": tool_use_id, "content": str(result) }]}) # Continue the conversation final_response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, tools=[...], messages=messages )

stop_sequence: Custom Stop Condition

When stop_reason is stop_sequence, Claude encountered a custom stop sequence you defined in your request. This is useful for structured output generation.

Handling stop_sequence

response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    stop_sequences=["\n\nEND"],
    messages=[{
        "role": "user",
        "content": "List three facts about Mars. End with 'END'."
    }]
)

if response.stop_reason == "stop_sequence": # The response was terminated by a stop sequence print(f"Stopped at sequence: {response.stop_sequence}") print(response.content[0].text)

Building a Robust Response Handler

For production applications, you should handle all stop reasons in a unified way:

def handle_claude_response(response, messages, client, tools=None):
    """Handle Claude's response based on stop_reason."""
    
    if response.stop_reason == "end_turn":
        # Claude is done. Return the final response.
        if not response.content:
            # Handle empty response
            messages.append({
                "role": "user",
                "content": "Please continue."
            })
            return client.messages.create(
                model=response.model,
                max_tokens=1024,
                messages=messages
            )
        return response
    
    elif response.stop_reason == "max_tokens":
        # Response was truncated. Continue generation.
        messages.append({"role": "assistant", "content": response.content})
        messages.append({"role": "user", "content": "Please continue."})
        return client.messages.create(
            model=response.model,
            max_tokens=1024,
            messages=messages
        )
    
    elif response.stop_reason == "tool_use":
        # Claude wants to use tools. Execute and continue.
        messages.append({"role": "assistant", "content": response.content})
        
        for block in response.content:
            if block.type == "tool_use":
                result = execute_tool(block.name, block.input)
                messages.append({"role": "user", "content": [{
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": str(result)
                }]})
        
        return client.messages.create(
            model=response.model,
            max_tokens=1024,
            tools=tools,
            messages=messages
        )
    
    elif response.stop_reason == "stop_sequence":
        # Custom stop sequence encountered. Process as needed.
        return response
    
    else:
        raise ValueError(f"Unknown stop_reason: {response.stop_reason}")

Best Practices

  • Always check stop_reason before processing response content. Don't assume end_turn.
  • Handle empty responses gracefully by adding continuation prompts rather than retrying the same input.
  • Set appropriate max_tokens based on your expected output length. For long-form content, use higher values.
  • Implement a loop for tool use since Claude may call multiple tools in sequence.
  • Log stop_reason and stop_sequence for debugging and monitoring.

Key Takeaways

  • Four stop reasons exist: end_turn (natural completion), max_tokens (truncated), tool_use (wants to call a tool), and stop_sequence (custom stop encountered).
  • Handle empty end_turn responses by adding a continuation prompt in a new user message, not by retrying the same input.
  • For max_tokens, append Claude's partial response and ask it to continue to get the full output.
  • For tool_use, execute the requested tool and send the result back in a tool_result block without additional text.
  • Build a unified handler that processes all stop reasons to create robust, production-ready applications.