What you will build

An AI HTTP client should not retry every failure or sleep beyond the time its caller can tolerate. Isolate the retry decision from network I/O so it can be tested with a fixed clock. This example returns a delay in seconds or None to stop. All HTTP statuses and headers are fixtures; it sends no requests and performs no retry sleeps.

The application policy permits retries only for 429 and 503 and allows at most three total attempts. This is a deliberately narrow policy for operations whose replay behavior has been reviewed. A repeated generation can produce a different answer and can be billed again; the planner does not make a request idempotent.

Prerequisites

Python 3.12 or later. Only standard-library modules are needed. The caller supplies trusted attempt counts, a finite remaining wait budget and an aware UTC clock value. Save as retry.py.

import re
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime

def retry_delay(status, header, attempt, remaining, now):
    if type(attempt) is not int or not 1 <= attempt <= 3 or remaining < 0:
        raise ValueError('invalid retry budget')
    if status not in (429, 503) or attempt == 3:
        return None
    delay = min(2 ** (attempt - 1), 8)
    if header is not None:
        text = header.strip()
        if re.fullmatch(r'[0-9]+', text):
            # Compare decimal strings before converting untrusted huge integers.
            canonical = text.lstrip('0') or '0'
            if len(canonical) > 3:
                return None
            delay = int(canonical)
        else:
            try:
                date = parsedate_to_datetime(text)
                if date.tzinfo is None:
                    return None
                delay = max(0, (date - now).total_seconds())
            except (ValueError, TypeError, OverflowError):
                return None
    return delay if delay <= remaining else None

now = datetime(2026, 11, 14, tzinfo=timezone.utc)
assert retry_delay(429, '3', 1, 5, now) == 3
assert retry_delay(503, 'Sat, 14 Nov 2026 00:00:04 GMT', 1, 5, now) == 4
assert retry_delay(503, 'Fri, 13 Nov 2026 00:00:00 GMT', 1, 5, now) == 0
assert retry_delay(503, None, 2, 5, now) == 2
for status, header, attempt, budget in [(401, None, 1, 5),
    (429, '6', 1, 5), (503, 'bad', 1, 5), (503, None, 3, 5),
    (429, '9' * 5000, 1, 5)]:
    assert retry_delay(status, header, attempt, budget, now) is None
print('retry=3s date=4s fallback=2s; stop fixtures passed')

Run and expected output

python3 retry.py
retry=3s date=4s fallback=2s; stop fixtures passed

How it works

HTTP Retry-After supports a nonnegative decimal delay or an HTTP date. The planner accepts ASCII digits for the first form. Before converting, it removes leading zeroes and rejects more than three remaining digits, placing a local ceiling of 999 seconds on numeric header values. This avoids handing an enormous untrusted integer string to conversion.

For dates, parsedate_to_datetime supplies the datetime. An absent timezone or malformed value stops retries. Past dates become zero delay. Future dates are compared against the injected clock, so tests do not depend on machine time.

Without a header, the first two retry opportunities use one and two seconds. If a valid server delay exceeds the remaining budget, the planner stops rather than shortening the requested wait and retrying early. The final attempt always stops.

Edge cases and scope

A zero-second result is a valid immediate retry; use delay is None when checking the result. A truthiness test would confuse zero with stopping. This budget covers waiting only. A production orchestrator must also budget network time, generation and decoding, generally against a monotonic deadline. Recompute remaining time after each operation.

Malformed headers stop this sample even if fallback would otherwise be possible. A fleet client may add jitter to fallback waits, but must preserve server minimum delays. Those orchestration behaviors are outside this pure function. Its input contract also assumes that HTTP header-size limits are enforced by the transport.

Troubleshooting

If a date unexpectedly stops, inspect timezone information and verify the injected clock is aware. If calls continue after three attempts, the caller is not advancing the attempt count. Review replay semantics before integrating this with write-capable tools. Authentication failures such as 401 intentionally return no retry, making configuration errors visible promptly.

Primary references

Continue with Python tutorials and all Java Samples categories.