Mastering Claude API Stop Reasons: Build Robust Applications with end_turn, max_tokens & tool_use
Learn how to handle Claude API stop_reason values like end_turn, max_tokens, and tool_use. Includes code examples, empty response fixes, and best practices.
This guide explains Claude API's stop_reason field—end_turn, max_tokens, tool_use, and stop_sequence—with practical code examples and strategies to handle empty responses, tool call loops, and truncated output.
Mastering Claude API Stop Reasons: Build Robust Applications
Every time you call the Claude API, the response includes a stop_reason field. This small piece of data tells you why the model stopped generating—whether it finished naturally, hit a token limit, requested a tool call, or encountered a 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 them in your code (Python & TypeScript)
- How to prevent and recover from empty responses
- Best practices for tool-using agents and streaming
What is stop_reason?
The stop_reason field appears in every successful Messages API response. It is not an error—it’s a signal about why Claude finished its response generation. Here’s a typical response:
{
"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
}
}
The Four Stop Reason Values
1. end_turn — Natural Completion
This is the most common stop reason. Claude finished its response naturally and expects no further action. In most cases, you can simply display the response to the user.
Python example: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)
TypeScript example:
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic();
const response = await client.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Hello!' }]
});
if (response.stop_reason === 'end_turn') {
console.log(response.content[0].text);
}
2. max_tokens — Token Limit Reached
Claude stopped because it hit the max_tokens limit you set. The response may be truncated—the model had more to say but was cut off.
- Increase
max_tokensif you need longer responses. - Or, send a follow-up message like
"Please continue"to let Claude finish.
if response.stop_reason == "max_tokens":
# Option 1: Increase max_tokens and retry
# Option 2: Ask Claude to continue
messages.append({"role": "user", "content": "Please continue"})
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2048,
messages=messages
)
3. tool_use — Claude Wants to Call a Tool
When you provide tools to Claude, it may stop to request a tool call. The response content will contain one or more tool_use blocks.
tool_result block. Do not add extra text after the tool_result—that can cause empty responses (see below).
if response.stop_reason == "tool_use":
for block in response.content:
if block.type == "tool_use":
# Execute the tool
result = execute_tool(block.name, block.input)
# Append tool_result to messages
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result)
}]
})
# Now call Claude again with the tool result
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages
)
4. stop_sequence — Custom Stop Sequence Hit
If you provided custom stop_sequences in your API request, Claude will stop when it encounters one. The stop_sequence field will contain the exact sequence that triggered the stop.
if response.stop_reason == "stop_sequence":
print(f"Stopped at sequence: {response.stop_sequence}")
# Handle accordingly
Handling Empty Responses with end_turn
A common pitfall: Claude returns an empty response (2–3 tokens, no content) with stop_reason: "end_turn". This usually happens in tool-use workflows.
Why it happens
- Adding text after
tool_result: Claude learns that the user always inserts text after tool results, so it ends its turn to follow the pattern. - Sending Claude’s completed response back unchanged: Claude already decided it’s done, so it stays done.
How to prevent it
Incorrect — adding text aftertool_result:
messages = [
{"role": "user", "content": "Calculate 1234 + 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 only the tool_result:
messages = [
{"role": "user", "content": "Calculate 1234 + 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 result
]}
]
Recovering from empty responses
If you still get an empty response, do not retry with the same messages—Claude will remain stuck. Instead, add a new user message asking it to continue:
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:
# Add a continuation prompt
messages.append({"role": "user", "content": "Please continue"})
response = client.messages.create(
model="claude-opus-4-7",
max_tokens=1024,
messages=messages
)
return response
Best Practices for Production
1. Always check stop_reason
Don’t assume end_turn. Build a switch or if-else chain to handle each case:
match response.stop_reason:
case "end_turn":
display_response(response)
case "max_tokens":
request_continuation(response)
case "tool_use":
execute_tools_and_continue(response)
case "stop_sequence":
handle_custom_stop(response)
2. Handle streaming responses
In streaming mode, the final message will contain the stop_reason. Accumulate content and check at the end:
with client.messages.stream(...) as stream:
for event in stream:
if event.type == "message_stop":
stop_reason = event.message.stop_reason
# Handle accordingly
3. Set appropriate max_tokens
For open-ended tasks, use a generous limit (e.g., 4096). For structured outputs, set a lower limit to avoid wasted tokens.
4. Log stop reasons for debugging
Track stop_reason in your logs to identify patterns—like frequent max_tokens stops indicating you need higher limits.
Summary of Stop Reasons
| stop_reason | Meaning | Action Required |
|---|---|---|
end_turn | Natural completion | Display response |
max_tokens | Token limit hit, response truncated | Increase limit or ask to continue |
tool_use | Claude wants to call a tool | Execute tool, return result |
stop_sequence | Custom stop sequence encountered | Handle based on your logic |
Key Takeaways
- Always check
stop_reasonin your API responses—it tells you exactly why Claude stopped and what to do next. - Never add text after
tool_resultblocks; it causes empty responses. Send only the tool result. - Recover from empty responses by adding a new user message like
"Please continue"—don’t retry the same messages. - For
max_tokensstops, either increase the limit or ask Claude to continue; the response is truncated. - Log stop reasons in production to monitor behavior and optimize your API usage.