deniz.in

Markets

Weather

Loading weather

· via dev.to (home feed)

Claude API returns empty output when thinking tokens consume the max_tokens budget

A dev.to post documents how Claude's thinking tokens can consume the entire max_tokens budget, returning HTTP 200 with empty output and no error, alongside a broader triage guide for Claude API failures.

Claude API returns empty output when thinking tokens consume the max_tokens budget

An HTTP 200 with nothing in it

According to a post on dev.to by Sungwoo Lee, the most confusing failure in the Claude API is not a 400, a 429 or a 529. It is a successful-looking HTTP 200 whose text block comes back empty. No exception is raised, nothing is logged as an error, and the first sign of trouble is a downstream JSON parser reporting that it found no JSON.

In the case described in the post, a request for a JSON content plan returned an empty string. The cause: thinking tokens draw from the same max_tokens budget as the answer. Of 5,369 output tokens, 4,474 went to thinking, leaving 895 for the response itself. The call ended with stop_reason set to "max_tokens" and a truncated or missing text block, all behind a status 200.

A misdiagnosis worth avoiding

The author describes a four-step detour before finding the real cause. The log said JSON parsing had failed. The team assumed the prompt was steering the model toward prose instead of JSON, and rewrote it. The same failure came back three times in a row. Only then did they identify the output budget as the problem.

The discriminator, per the post, is the raw response. If the prompt had genuinely pushed the model away from JSON, the raw output would still contain text. Only a completely empty raw string supports the diagnosis that thinking consumed the budget.

Where to look and what to change

The field to read is usage.output_tokens_details.thinking_tokens, alongside stop_reason. When streaming, the post notes, the thinking-token count appears only on the final message_delta event, so it is easy to miss if that event is not inspected.

Two official fixes exist: raise max_tokens so that thinking and the answer both fit, or lower the thinking effort so reasoning spends less. Lee's team added a third: shrink what a single call has to produce. Instead of asking for a full multi-post plan at once, they request one outline and then one table of contents per post, and the call that had failed three times passed on the first try. The rule of thumb offered: if the output must arrive in one piece, use the official fixes; if it can be split, split it.

A wider triage order for Claude errors

The post frames the empty-response case inside a general check order:

  • Branch on error.type, never on message text. Anthropic's documentation says wording can change, and the SDK raises a distinct exception class per status code, so a string comparison on the message will eventually break.
  • A 400 means the request itself is wrong, and retrying unchanged returns the same result. Triggers the post singles out on newer models include prefilled assistant messages, the removed fixed thinking budget (replaced by adaptive thinking with an effort setting), thinking blocks from earlier turns that were edited or dropped, and forced tool_choice settings.
  • A 429 is three different problems: a rate limit, a monthly spend cap for the usage tier, or a Claude Code workspace spend limit. The retry-after header is the tell. Rate limits send it; spend caps do not and keep failing until access reopens. Treating a cap as a rate limit produces a backoff loop that receives the same 429 all day.
  • A 529 signals platform-wide overload, not an account problem, so wait and retry. For 500 and 504, the SDK has already retried twice with exponential backoff before the exception reaches application code; requests that may run long belong on streaming or the batch API.
  • In Claude Code, an MCP tool that fails only on its first call in a session suggests a cold start, and the post points to raising MCP_TIMEOUT. Its sharpest caution: never blindly retry a timed-out write, because a timeout means the outcome is unknown, and resending a write that already landed applies the change twice.

Why it matters

Silent failures are harder to catch than loud ones. Any pipeline running Claude with extended thinking enabled, especially one expecting structured JSON output, can hit this mode where a status-code check happily reports success while producing nothing. Logging thinking_tokens and stop_reason on every response turns the failure visible. The post also models good API error handling in general: classify by machine-readable type, separate retryable from non-retryable causes before writing a backoff loop, and treat unknown outcomes differently from confirmed failures.

  • #anthropic
  • #claude-api
  • #llm
  • #api-errors
  • #debugging

Related posts