Transient failures (resets, 502, 503, 429) are normal at scale; a retry policy turns them into non-events.
Requests does not retry failed requests by default (it retries nothing beyond what urllib3 does internally for a dead pooled connection). To add a policy, build a urllib3.util.Retry and attach it to a transport adapter mounted on a session:
retry = Retry(total=5, backoff_factor=0.5, status_forcelist=[429, 500, 502, 503, 504])
s.mount("https://", HTTPAdapter(max_retries=retry))Important Retry options:
total,connect,read,status: overall and per-category limits.backoff_factor: exponential backoff. The first retry happens immediately, then urllib3 sleepsbackoff_factor * 2 ** (n - 1)seconds, where n counts consecutive failures, so 0.1 gives 0, 0.2, 0.4 and so on, capped bybackoff_max. Addbackoff_jitterto avoid synchronised retry storms.status_forcelist: response statuses that should be retried.allowed_methods: by default only idempotent methods (GET, HEAD, PUT, DELETE, OPTIONS, TRACE) are retried.POSTis excluded because repeating it could create duplicates. Add it only if the API supports idempotency keys.respect_retry_after_header(defaultTrue): honoursRetry-Afteron 429 and 503.raise_on_status: when retries are exhausted on a status, raiserequests.exceptions.RetryError(default) or return the last response.
Retries multiply worst-case latency: each attempt can wait up to the timeout, plus backoff. Budget total time accordingly, and log retries so silent degradation is visible.
Example
import time
import requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
retry = Retry(
total=3,
backoff_factor=0.1,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods={"GET", "PUT", "DELETE"},
)
with requests.Session() as s:
s.mount("https://", HTTPAdapter(max_retries=retry))
s.mount("http://", HTTPAdapter(max_retries=retry))
t = time.perf_counter()
try:
s.get("https://httpbin.org/status/503", timeout=5)
except requests.exceptions.RetryError as e:
print("gave up:", type(e).__name__) # gave up: RetryError
print(f"{time.perf_counter() - t:.1f}s with backoff") # about 0.6s
r = s.post("https://httpbin.org/status/503", timeout=5)
print(r.status_code) # 503 (POST is not retried)Remember
- Mount HTTPAdapter(max_retries=Retry(...)) on a session to retry
- status_forcelist and backoff_factor define what and how fast to retry
- Only idempotent methods are retried by default; POST needs care
Watch out for
- Retrying POST requests blindly and creating duplicate orders or payments.