← Blog

Streaming is required for operations that may take longer than 10 minutes: the Anthropic SDK error, explained

The Anthropic TypeScript SDK refuses non-streaming calls above 21,333 max_tokens before sending. Where that limit comes from, and the streaming fix.

The kit's text helper caps output at 16,000 tokens, and when a response hits that cap it throws with a hint: "Raise maxTokens, or use chatStream() for long outputs." On Claude Opus 5.5, which always thinks and spends that same budget on its reasoning, raising it is the natural move. Set it to 32,000 and the call fails instantly with Streaming is required for operations that may take longer than 10 minutes. No request is sent and no retry happens. The Anthropic TypeScript SDK refuses it locally, based on nothing but the max_tokens number. Here is exactly where that line sits, which calls hit it, and the fix that keeps the helper's signature.

Where streaming is required: the 21,333-token line

In @anthropic-ai/sdk 0.104.1, messages.create checks every non-streaming request before it builds the HTTP call:

// resources/messages/messages.js, inside create()
let timeout = this._client._options.timeout;
if (!body.stream && timeout == null) {
  const maxNonstreamingTokens = MODEL_NONSTREAMING_TOKENS[body.model] ?? undefined;
  timeout = this._client.calculateNonstreamingTimeout(body.max_tokens, maxNonstreamingTokens);
}

// client.js
calculateNonstreamingTimeout(maxTokens, maxNonstreamingTokens) {
  const maxTime = 60 * 60 * 1000; // 60 minutes
  const defaultTime = 60 * 10 * 1000; // 10 minutes
  const expectedTime = (maxTime * maxTokens) / 128000;
  if (expectedTime > defaultTime || (maxNonstreamingTokens != null && maxTokens > maxNonstreamingTokens)) {
    throw new Errors.AnthropicError('Streaming is required for operations that may take longer than 10 minutes. ...');
  }
  return defaultTime;
}

The SDK budgets an hour per 128,000 output tokens, and refuses anything it expects to run past its 10-minute default timeout. Ten minutes is a sixth of an hour, so the line is 128,000 / 6 = 21,333.3 tokens. max_tokens: 21_333 goes out and 21_334 throws. The per-model table, MODEL_NONSTREAMING_TOKENS, only lists Opus 4 and Opus 4.1 snapshots (capped at 8,192) in this version, so for current models the formula is the only limit. Opus 5.5 accepts up to 128,000 output tokens, which means a non-streaming call can ask for one sixth of what the model can write.

The guard has a reason: a non-streaming response sends nothing back until the whole answer is done, so the connection sits silent for as long as the model writes. Streaming keeps bytes flowing.

Measured: which calls hit it

I ran each case against the installed SDK with a fake key and baseURL pointed at a closed local port. A call that passes the check fails with APIConnectionError; a call the SDK refuses fails with AnthropicError in 0 ms.

  • messages.create, max_tokens 16,000 and 21,333: connection error, so the request was sent.
  • messages.create, 21,334 and 64,000: AnthropicError, nothing sent.
  • 64,000 plus a per-request { timeout: 3_600_000 }: still AnthropicError. The check reads the client's options, not the request's, so the per-request override never gets a chance.
  • messages.parse at 64,000: AnthropicError. It calls create underneath.
  • A client built with new Anthropic({ timeout: 3_600_000 }): connection error. A client-level timeout skips the check entirely.
  • messages.stream(...).finalMessage() at 64,000: connection error. Streaming is never checked.

Then the kit's own helpers, which share one client built with only { maxRetries: 3 }. With maxTokens: 32_000, generateText, generateObject and runWithTools all threw AnthropicError within 3 ms. The default 16,000 went out normally and failed only after three connection retries, about 3 seconds.

The fix: stream, then take the final message

The helpers do not need streaming output. They need the request to be a streaming request. The SDK's finalMessage() collects the stream into the same Message that create returns:

export async function generateText(opts: GenerateOptions): Promise<GenerateTextResult> {
  const model = opts.model ?? DEFAULT_MODEL;
  const response = await anthropic()
    .messages.stream({
      model,
      max_tokens: opts.maxTokens ?? 16_000,
      ...adaptiveThinking(model),
      ...(opts.system ? { system: opts.system } : {}),
      messages: opts.messages,
    })
    .finalMessage();
  assertCompleted(response);
  return {
    text: messageText(response),
    usage: usageFromMessage(response.usage),
    stopReason: response.stop_reason,
    raw: response,
  };
}

generateObject takes the same change. messages.stream accepts output_config: { format: zodOutputFormat(schema) }, and its finalMessage() resolves to a parsed message with parsed_output, so the structured-output helper keeps its typed result. Both versions type-check against the kit, and at max_tokens: 64_000 both got past the check to the connection error. The kit's helpers still call create and parse today. This is the change I would make, because a client-level timeout would also silence the error, but it removes the guard for every non-streaming call in the app and leaves the long silent connection in place.

Things that bite

  • It is not an API error. The throw is a plain AnthropicError, not an APIError, so it has no status. Error handling that branches on err instanceof Anthropic.APIError or reads err.status will file it under "unknown", and the SDK's retries never see it.
  • It throws synchronously. create throws before it returns a promise. await inside a try catches it, but client.messages.create(body).catch(handler) does not: in my test the error escaped and the .catch handler never ran.
  • Thinking shares the budget you are raising. On Opus 5.5 thinking cannot be turned off and counts against max_tokens, as covered in the thinking parameter guide. The kit's generateObject sends no thinking field, so on Opus 4.8 it did not think at all. On Opus 5.5 it thinks inside the same 16,000 tokens, which makes it the helper where you are most likely to raise the limit first.
  • Tool loops call it once per turn. The kit's agent loop calls messages.create on every iteration, so a raised maxTokens fails on the first one. Swap in stream(...).finalMessage() there too, and keep summing usage per iteration as before.

Shipwright, the Next.js and Claude starter kit this blog documents, routes every model call through four helpers, so a change like this lands in one file instead of every route. The chat itself already streams; watch it in the live demo.