Python SDK: retry_on_overload jitter can push sleep beyond max_delay_s
Description
retry_on_overload() in sdk/python/src/openai_codex/retry.py computes the jitter window from the uncapped delay, but adds it to the capped base:
delay = initial_delay_s
attempt = 0
while True:
attempt += 1
try:
return op()
except Exception as exc:
...
jitter = delay * jitter_ratio
sleep_for = min(max_delay_s, delay) + random.uniform(-jitter, jitter)
if sleep_for > 0:
time.sleep(sleep_for)
delay = min(max_delay_s, delay * 2)
Nothing in the function validates initial_delay_s <= max_delay_s (only max_attempts >= 1 is checked), and this is a public, documented function (openai_codex.retry_on_overload, also used internally by CodexClient.request_with_retry_on_overload / AsyncCodexClient.request_with_retry_on_overload, both of which expose initial_delay_s/max_delay_s as caller-tunable kwargs with no cross-validation either). If a caller passes initial_delay_s > max_delay_s, the first sleep's jitter is computed from the uncapped delay, so the actual sleep can exceed the documented max_delay_s ceiling.
Steps to reproduce
import random, time
from openai_codex.retry import retry_on_overload
import openai_codex.retry as retry_mod
retry_mod.is_retryable_error = lambda exc: True # force retry path
calls = []
time.sleep = calls.append
attempts = {"n": 0}
def op():
attempts["n"] += 1
if attempts["n"] < 3:
raise RuntimeError("boom")
return "ok"
random.seed(1)
retry_on_overload(op, max_attempts=3, initial_delay_s=10.0, max_delay_s=2.0, jitter_ratio=0.2)
print(calls)
Output:
[0.5374569764496049, 2.277946989549786]
The second sleep (2.2779...) exceeds the configured max_delay_s=2.0.
Expected behavior
No sleep should ever exceed max_delay_s, regardless of initial_delay_s.
Actual behavior
The first sleep can exceed max_delay_s when initial_delay_s > max_delay_s, because the jitter window is derived from the uncapped delay while the base is capped.
Environment
- Commit:
7c37479(main, 2026-08-27), Python 3.14 - File:
sdk/python/src/openai_codex/retry.py:37-38 - Note:
retry_on_overloadcurrently has no dedicated unit test file (only referenced intests/test_public_api_signatures.pyfor signature checks, plus docs/examples) — this edge case isn't covered anywhere.
Suggested fix
- jitter = delay * jitter_ratio
- sleep_for = min(max_delay_s, delay) + random.uniform(-jitter, jitter)
+ base_delay = min(max_delay_s, delay)
+ jitter = base_delay * jitter_ratio
+ sleep_for = base_delay + random.uniform(-jitter, jitter)
1 Comment
Hi! I’d like to work on this. I reproduced the issue and traced it to the jitter calculation in retry_on_overload(). I’ll add a fix to ensure the final sleep duration respects max_delay_s, along with regression tests.