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.
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.
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_reasonvalue 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_tokensandstop_sequence
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:
| Value | Meaning |
|---|---|
end_turn | Claude finished naturally and expects no further input |
tool_use | Claude wants to call a tool (function) |
max_tokens | Claude stopped because it hit the max_tokens limit |
stop_sequence | Claude 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_resultblocks - You send back Claude’s completed response 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 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_tokensfor 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_turnis the normal completion signal; watch for empty responses in tool workflows and fix by removing extra text aftertool_resultblocks.tool_userequires you to execute the tool and return results; always check for multiple tool calls in a single response.max_tokensmeans the response was truncated; continue the conversation with a higher limit or ask Claude to finish.stop_sequenceindicates 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.