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.
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.
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_reasonvalues 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:
| Value | Meaning |
|---|---|
end_turn | Claude finished its response naturally |
max_tokens | Claude hit the max_tokens limit you set |
tool_use | Claude wants to use a tool (function calling) |
stop_sequence | Claude encountered a custom stop sequence you defined |
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.
- Adding text blocks immediately after tool results
- Sending Claude's completed response back without adding anything new
# 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_reasonbefore processing response content. Don't assumeend_turn. - Handle empty responses gracefully by adding continuation prompts rather than retrying the same input.
- Set appropriate
max_tokensbased 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_reasonandstop_sequencefor debugging and monitoring.
Key Takeaways
- Four stop reasons exist:
end_turn(natural completion),max_tokens(truncated),tool_use(wants to call a tool), andstop_sequence(custom stop encountered). - Handle empty
end_turnresponses 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 atool_resultblock without additional text. - Build a unified handler that processes all stop reasons to create robust, production-ready applications.