HTTP & LLM Helpers¶
pyresilience.contrib.http provides stdlib-only building blocks for the most common resilience
need on the internet today: handling HTTP 429 rate limits and Retry-After headers when
calling REST and LLM APIs.
The helpers are duck-typed — they work with requests, httpx, and aiohttp response objects
without importing any of them. The module has zero dependencies beyond the Python standard
library, so the core package's zero-deps promise holds.
Note
These helpers live under pyresilience.contrib.http and are not re-exported from the
package root, matching the other contrib integrations. The llm_policy()
preset wires them up for you.
retry_on_status(*codes)¶
Builds a predicate for RetryConfig.retry_on_result that
returns True when a response's status code matches one of codes.
from pyresilience import resilient, RetryConfig
from pyresilience.contrib.http import retry_on_status
@resilient(retry=RetryConfig(
max_attempts=4,
retry_on_result=retry_on_status(429, 500, 502, 503, 504),
))
def call_api():
return requests.get("https://api.example.com/data") # don't raise_for_status()
Behavior:
- Reads
response.status_code(requests/httpx) first, thenresponse.status(aiohttp). - Only genuine integers match — strings like
"429"andboolvalues are rejected. - Returns
Falsefor anything that isn't response-shaped, so it's safe with mixed return types. - Raises
ValueErrorwhen called with no codes andTypeErrorfor non-integer codes.
retry_after_delay(max_wait=60.0)¶
Builds a delay_func that reads the Retry-After
header from the object that triggered the retry and waits exactly as long as the server asked —
instead of guessing with exponential backoff.
from pyresilience import resilient, RetryConfig
from pyresilience.contrib.http import retry_on_status, retry_after_delay
@resilient(retry=RetryConfig(
max_attempts=4,
delay=1.0, # backoff base when no Retry-After is present
retry_on_result=retry_on_status(429, 503),
delay_func=retry_after_delay(max_wait=60.0),
))
def call_api():
return requests.get("https://api.example.com/data")
Behavior:
- Header sources, in order:
trigger.headers, thentrigger.response.headers(coversrequests.HTTPError-shaped exceptions raised byraise_for_status()). - Parses both
Retry-Afterforms: delta-seconds ("30") and HTTP-date ("Wed, 21 Oct 2026 07:28:00 GMT"). - Returns
Nonewhen the header is missing or unparseable — pyresilience then falls back to the configured exponential backoff. Your retries never break because of a weird header. - The returned delay is clamped to
[0, max_wait], so a misbehaving server can't make your client sleep for an hour. - Raises
ValueErrorat build time whenmax_wait <= 0.
Putting It Together for LLM APIs¶
For OpenAI/Anthropic-style clients that raise exceptions on 429 instead of returning responses,
attach the helpers to the exception side — retry_after_delay already understands exceptions
carrying a .response attribute:
from pyresilience import resilient, RetryConfig, CircuitBreakerConfig
from pyresilience.contrib.http import retry_after_delay
@resilient(
retry=RetryConfig(
max_attempts=4,
delay=1.0,
retry_on=(RateLimitError, APIConnectionError), # retryable
ignore_on=(AuthenticationError, PermissionDeniedError), # terminal: fail fast
delay_func=retry_after_delay(max_wait=60.0),
),
circuit_breaker=CircuitBreakerConfig(
failure_threshold=5,
ignore_on=(AuthenticationError, PermissionDeniedError), # don't trip the circuit
),
)
def ask_model(prompt: str):
return client.chat.completions.create(model="...", messages=[{"role": "user", "content": prompt}])
Or skip the wiring entirely with the llm_policy() preset, which
combines all of the above with a client-side rate limiter: