Claude extended_thinking — budget exceeded, no final answer returned
Extended thinking (Claude 3.7+ and 4.x) counts against your <code>max_tokens</code>. When reasoning drains the budget, the response finishes with <code>stop_reason: "max_tokens"</code> and zero output text — this page explains how to size the split correctly.
Quick fix (TL;DR)
thinking.type = "enabled", Claude generates reasoning tokens before the final answer. Both share max_tokens. If thinking.budget_tokens is set close to or equal to max_tokens, the model may exhaust its budget on reasoning and return no visible answer, or truncate mid-answer. Fix by (a) sizing max_tokens as thinking.budget_tokens + expected_answer_tokens + 500, (b) reading stop_reason to detect truncation, and (c) never setting the thinking budget above 60-70% of max_tokens.Real error messages you'll see
These are the exact strings returned by the Claude API service and its SDKs when this error occurs. Copy-paste-searching any of them should land on this page.
Message(
id="msg_...",
stop_reason="max_tokens",
content=[
ThinkingBlock(type="thinking", thinking="The user asked... I should first ..."),
# no TextBlock — response was cut before generating
],
usage=Usage(input_tokens=124, output_tokens=8000, cache_creation_input_tokens=0)
)anthropic.BadRequestError: Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': 'thinking.budget_tokens (10000) must be less than max_tokens (8000).'}}anthropic.BadRequestError: Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': 'thinking.budget_tokens must be at least 1024 when thinking is enabled.'}}
Reference
How the budget splits
Reasoning tokens and output tokens both count against max_tokens. The model tries to leave room but is not guaranteed to.
| Setting | What it controls | Typical value | Failure mode when too low |
|---|---|---|---|
thinking.budget_tokens | Soft cap on reasoning | 4,000 – 16,000 | Reasoning cut short → weaker answer |
max_tokens | Hard cap on reasoning + answer | budget × 1.5 to 2× | Answer truncated or missing |
| answer headroom (implicit) | max_tokens − budget − prompt_effect | 2,000 – 8,000 | Cannot fit final answer |
Which Claude models support extended thinking
| Model family | Extended thinking | Default budget | Notes |
|---|---|---|---|
| Claude 4.6 / 4.7 Opus | Yes | None — must opt in | Longest reasoning window |
| Claude 4.6 / 4.7 Sonnet | Yes | None — must opt in | Best cost/quality for reasoning |
| Claude Haiku 4.5 | Yes | None — must opt in | Fast reasoning; small budget |
| Claude 3.5 Sonnet / Haiku | No | n/a | Non-thinking family |
| Claude 3 (retired) | No | n/a | Migrate — see page 90 |
Root causes, ranked by frequency
Based on developer reports across Claude API forums, GitHub issues, and Anthropic community during 2025–2026.
- 28%budget_tokens set equal to or greater than max_tokens. Off-by-one at request build time; hard 400.
- 22%Complex prompt drains most of the budget on reasoning. Model spends 7,900 of 8,000 tokens thinking, returns 100 tokens of answer or nothing at all.
- 16%No headroom for the answer. max_tokens = 8,000, budget = 7,500 leaves 500 for the answer — insufficient for anything but yes/no.
- 12%Streaming client not distinguishing thinking blocks from text blocks. UI shows nothing because thinking blocks arrive first and are not rendered as text.
- 8%Reasoning explored a dead end. Model spent budget on wrong track; noticed too late; ran out of budget backing up. Prompt engineering issue.
- 7%Budget too low for the task complexity. Multi-step math or code review with a 1,024-token budget produces empty or shallow reasoning.
- 5%Retry with same params. Non-determinism helps sometimes but is not a real fix — expected to fail on the same shape.
- 2%Interleaved thinking (Claude 4.6+) misconfigured. Model uses thinking between tool calls; each thinking block eats budget.
Fixes — copy-paste solutions
Size max_tokens using the budget-plus-headroom formula
Compute max_tokens = budget_tokens + expected_answer_tokens + safety_margin. Never set them equal. Never let budget exceed 70% of max_tokens for open-ended tasks.
"""Size max_tokens for extended thinking, safely.""" import anthropic client = anthropic.Anthropic() def call_with_thinking(prompt: str, task_complexity: str = "medium", expected_answer_tokens: int = 2000): """ task_complexity: 'simple' | 'medium' | 'complex' | 'research' Determines the reasoning budget. """ budgets = { "simple": 2_000, # yes/no with light reasoning "medium": 6_000, # code review, single-hop analysis "complex": 16_000, # multi-hop planning, math "research": 32_000, # deep reasoning tasks (Opus recommended) } budget = budgets[task_complexity] safety_margin = 500 # room for token accounting slop max_tokens = budget + expected_answer_tokens + safety_margin # Never let budget exceed 70% of max_tokens if budget / max_tokens > 0.70: max_tokens = int(budget / 0.70) + safety_margin return client.messages.create( model="claude-opus-4-7", max_tokens=max_tokens, thinking={"type": "enabled", "budget_tokens": budget}, messages=[{"role": "user", "content": prompt}], ) response = call_with_thinking( "Prove that there are infinitely many prime numbers, from scratch.", task_complexity="complex", expected_answer_tokens=3000, ) # Extract the final text (skip thinking blocks) text = "".join(b.text for b in response.content if b.type == "text") reasoning = "".join(b.thinking for b in response.content if b.type == "thinking") print(f"Reasoning: {len(reasoning)} chars; Answer: {len(text)} chars") print(f"stop_reason: {response.stop_reason}")
Detect truncation from stop_reason and retry with a larger budget
Always inspect stop_reason. If it is max_tokens, the answer is truncated (or missing). Retry with a bigger max_tokens before returning to the user.
import anthropic from anthropic.types import Message client = anthropic.Anthropic() MAX_ESCALATIONS = 2 # bail after two retries to keep cost bounded def call_with_escalation(prompt: str, initial_budget: int = 6_000, initial_answer_headroom: int = 2_000) -> Message: budget = initial_budget headroom = initial_answer_headroom for attempt in range(MAX_ESCALATIONS + 1): response = client.messages.create( model="claude-opus-4-7", max_tokens=budget + headroom + 500, thinking={"type": "enabled", "budget_tokens": budget}, messages=[{"role": "user", "content": prompt}], ) if response.stop_reason != "max_tokens": return response # Grew reasoning by 50%, answer headroom by 2x budget = int(budget * 1.5) headroom = int(headroom * 2) print(f"[attempt {attempt}] truncated — retrying with " f"budget={budget}, headroom={headroom}") # Gave up — return the truncated response but flag it return response resp = call_with_escalation("Design a distributed rate limiter for 100M req/day.", initial_budget=8000, initial_answer_headroom=3000) print("stop_reason:", resp.stop_reason)
Stream and route thinking vs text blocks separately in your UI
Extended thinking can take 30-120 seconds silently. Stream the response and emit distinct UI events for thinking vs answer so the user sees progress.
import anthropic client = anthropic.Anthropic() def stream_with_thinking_events(prompt: str): """Yields {"type": "thinking"|"text"|"stop", ...} events.""" with client.messages.stream( model="claude-opus-4-7", max_tokens=12_000, thinking={"type": "enabled", "budget_tokens": 8_000}, messages=[{"role": "user", "content": prompt}], ) as stream: current_block_type = None for event in stream: if event.type == "content_block_start": current_block_type = event.content_block.type if current_block_type == "thinking": yield {"type": "thinking_start"} elif current_block_type == "text": yield {"type": "text_start"} elif event.type == "content_block_delta": delta = event.delta if delta.type == "thinking_delta": yield {"type": "thinking", "delta": delta.thinking} elif delta.type == "text_delta": yield {"type": "text", "delta": delta.text} elif event.type == "message_stop": yield {"type": "stop", "stop_reason": stream.current_message_snapshot.stop_reason} # UI-side rendering for event in stream_with_thinking_events("Solve: what is 17 * 43 without using multiplication."): if event["type"] == "thinking_start": print("[thinking...]", end="", flush=True) elif event["type"] == "thinking": # For internal debug or dedicated UI panel; do NOT show in main output pass elif event["type"] == "text_start": print("\n[answer:]\n", end="") elif event["type"] == "text": print(event["delta"], end="", flush=True) elif event["type"] == "stop": print(f"\n[stop_reason: {event['stop_reason']}]")
Prevention checklist
Ship these seven safeguards once and this error stops appearing in your logs.
- Use the formula:
max_tokens = budget_tokens + expected_answer_tokens + 500. - Keep
thinking.budget_tokensat ≤70% ofmax_tokensfor open-ended tasks. - Only enable extended thinking on tasks where quality demonstrably improves — measure first.
- Route thinking blocks to a UI panel or discard for output; only render text blocks in the answer.
- Always inspect
stop_reason; retry with escalated budget onmax_tokens. - Set an escalation cap (2 retries) to prevent runaway Opus cost on unbounded tasks.
- Log per-request thinking token counts — patterns reveal when the prompt is inducing bad reasoning paths.
Frequently asked questions
budget_tokens field is a soft cap — the model tries to stop by then but may overshoot slightly. To enforce a hard cap, size max_tokens to be exactly what you can afford; the model is forced to stop at that boundary regardless of the budget setting.Related errors & hubs
Get the weekly AI-error digest
New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.