Retries & backoff
parry retries transient failures with exponential backoff and jitter. It never retries errors that will fail again, such as an invalid key or a malformed request.
parry.install(max_attempts=3, retry_base_delay=0.5, retry_max_delay=30.0)parry.install(retry=False) # turn parry's retry layer offRetry schedule
What gets retried
Section titled “What gets retried”| Retried | Never retried |
|---|---|
RateLimitError (429) | BadRequestError (400) |
APITimeoutError, APIConnectionError | AuthenticationError (401) |
InternalServerError and every 5xx | PermissionDeniedError (403) |
| Anthropic overload (529) | NotFoundError (404) |
| 408, 409, 425 | UnprocessableEntityError (422) |
| httpx timeouts and connection errors | parry’s own BudgetExceeded / RateLimited |
Errors are classified by HTTP status when there is one, and by exception class
name otherwise. Retries exhausted? The SDK’s last exception is re-raised
unchanged, so your existing except openai.RateLimitError: blocks still
work.
The formula
Section titled “The formula”For the n-th failure (counting from 1):
raw = min(retry_max_delay, retry_base_delay × 2^(n-1))delay = raw/2 + random(0, raw/2) # "equal jitter": never an instant retryIf the response carries a retry-after-ms or retry-after header, that value
is used instead, capped at retry_max_delay.
Retries stack with the SDK’s own
Section titled “Retries stack with the SDK’s own”Pick one layer to own retries:
# parry is the only retry layer (recommended: consistent policy, counted in stats)client = openai.OpenAI(max_retries=0)
# or keep the SDK's retries and switch parry's offparry.install(retry=False)Retries and the other features
Section titled “Retries and the other features”- Each retry takes a fresh rate-limit slot.
- Every retry is counted in
stats.retries, and calls that finally fail instats.errors. - The budget is checked once, before the first attempt.
Standalone use
Section titled “Standalone use”from parry import Retry
retry = Retry(max_attempts=5, base_delay=0.25)result = retry.call(flaky_function, arg, key="value") # sleeps with the GIL releasedretry.delay(3) # inspect the scheduleRetry.is_retryable("RateLimitError", 429) # True