Retries
Exponential backoff with jitter, Retry-After awareness, and what gets retried
const llm = new VernLLM({
client: openai,
model: 'gpt-4o',
maxRetries: 3, // retries after the first attempt, so 4 attempts total
baseDelayMs: 500, // base for exponential backoff between retries
});Every call() retries transient failures automatically. maxRetries defaults to 1 (2 attempts total), so this is active even if you never touch the option.
Backoff and jitter
Each retry waits longer than the last, and the exact wait is randomized within a range rather than fixed:
exp = min(baseDelayMs * 2 ** attempt, maxDelayMs);
delay = exp / 2 + random() * (exp / 2);This is full jitter: the delay lands somewhere between half and all of the computed exponential value, instead of every caller waiting the identical amount. When many callers hit a failure at the same time, full jitter spreads their retries out instead of having them all retry in lockstep and hit the provider again at the same instant.
The delay is capped at DEFAULT_MAX_DELAY_MS (10 seconds) regardless of attempt, so a high maxRetries never produces an unbounded wait.
Honoring Retry-After
If the failed attempt's error carries a Retry-After header, that value is used for the wait instead of the computed backoff:
recoverDelay = retryAfterMs ?? getBackoffDelay(baseDelayMs, attempt);Retry-After is parsed from both delta-seconds form ("30") and HTTP-date form ("Wed, 21 Oct 2015 07:28:00 GMT"), checking .headers (fetch-style) then .response.headers (axios-style) so
it works across client libraries. The honored value is capped at the same max delay as backoff, so
a misbehaving or adversarial header can't stall a caller indefinitely.
What gets retried
shouldRetry skips a retry when any of these are true:
- The signal has already aborted
error.typeisparseorvalidation, since these are deterministic response processing failures that a retry will not fixerror.statusis innonRetryableStatus(default400, 401, 403, 404, 422)
Everything else, including timeouts, 5xx errors, network failures, and unknown errors, is retried up to maxRetries.
Exhausting all retries also records a circuit breaker failure if one is configured, and throws the
final normalized LLMError. See Error Handling for the full list of
error types and how each one is normalized.
Everything on this page describes retries against a single provider target. With fallback
configured, each declared target, primary and every fallback target, gets its own independent
maxRetries/baseDelayMs/nonRetryableStatus, resolved from that target's own overrides or
inherited from the instance when omitted. Exhausting one target's retries doesn't fail the call
outright, it hands off to fallbackOn to decide whether to try the next target. See Provider
Fallback.
Cancelling mid retry
If a signal fires while a retry is waiting out its backoff delay, the pending wait is cancelled immediately rather than sitting idle until the delay finishes. An abort during backoff is never retried, it is treated as a deliberate cancellation. See Cancellation & Timeouts for the full abort lifecycle.
Reading attempt history
Every normalized LLMError carries an optional attempts array. Each entry has the attempt's index and a snapshot of that attempt's error: an LLMErrorSnapshot with message, type, code, status, issues, retryAfterMs, retryable, and its own nested attempts if that attempt was itself the terminal failure of a retry loop. It's a snapshot, not a live LLMError, since a past attempt is a record, not something you'd catch or rethrow. cause isn't part of it. cause is meant to be read on the live error you actually caught (err.cause), not carried inside recorded history. Each entry can also carry request, an LLMRequestSnapshot of what was actually sent for that attempt, with auth headers always removed. See Error Handling for cause and for the full LLMRequestSnapshot shape.
try {
await llm.call({ userContent: 'hello' });
} catch (err) {
if (isLLMError(err)) {
for (const attempt of err.attempts ?? []) {
console.log(attempt.index, attempt.error.type);
}
}
}attempts is absent when nothing was retried, for example a call that failed on its first and only try. With fallback configured, each FallbackAttempt on a FallbackExhaustedError extends this same shape, adding provider and model, and that attempt's own error.attempts still holds the retries made against that one target. See Provider Fallback for FallbackExhaustedError itself.
Options reference
| Option | Default | Notes |
|---|---|---|
maxRetries | 1 | Retries after the first attempt. maxRetries: 3 means up to 4 attempts. |
baseDelayMs | 500 | Base for exponential backoff. Actual delay grows per attempt with jitter, and is overridden by an honored Retry-After. |
nonRetryableStatus | [400, 401, 403, 404, 422] | Status codes that fail immediately instead of retrying. |
See Configuration for every option alongside timeout and circuit breaker settings.