Python SDK: retry_on_overload jitter can push sleep beyond max_delay_s

Open 💬 1 comment Opened Aug 27, 2026 by xu-kai-quan

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_overload currently has no dedicated unit test file (only referenced in tests/test_public_api_signatures.py for 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)

View original on GitHub ↗

1 Comment

Abdulabin · 1 day ago

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.