Scenario
intermediate
Implementing Retry Logic with Exponential Backoff
Build robust retry logic for calls to an external payment API that occasionally returns 429 or 503, using exponential backoff with jitter.
The Situation
Senior Python developer / backend engineering interviews
Context: Your Python service calls an external payment API that occasionally returns HTTP 429 (rate limited) or 503 (service unavailable). How do you implement robust retry logic?
import time
import random
import functools
import logging
from typing import Type
logger = logging.getLogger(__name__)
def retry_with_backoff(
retryable_exceptions: tuple[Type[Exception], ...],
max_retries: int = 5,
base_delay: float = 1.0,
max_delay: float = 60.0,
jitter: bool = True,
):
"""Decorator for exponential backoff with jitter."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
delay = base_delay
for attempt in range(1, max_retries + 1):
try:
return func(*args, **kwargs)
except retryable_exceptions as e:
if attempt == max_retries:
logger.error(f"{func.__name__} failed after {max_retries} attempts: {e}")
raise
actual_delay = min(delay, max_delay)
if jitter:
# Add ±25% jitter to prevent thundering herd
actual_delay *= (0.75 + random.random() * 0.5)
logger.warning(
f"{func.__name__} attempt {attempt}/{max_retries} failed: {e}. "
f"Retrying in {actual_delay:.1f}s"
)
time.sleep(actual_delay)
delay *= 2 # Exponential backoff
return wrapper
return decorator
# Usage
import requests
class PaymentAPIError(Exception): pass
class RateLimitError(PaymentAPIError): pass
@retry_with_backoff(
retryable_exceptions=(RateLimitError, requests.Timeout, requests.ConnectionError),
max_retries=5,
base_delay=1.0,
)
def charge_payment(amount: float, card_token: str) -> dict:
response = requests.post(
"https://api.payment.com/charge",
json={"amount": amount, "token": card_token},
timeout=10,
)
if response.status_code == 429:
raise RateLimitError("Payment API rate limited")
if response.status_code >= 500:
response.raise_for_status()
return response.json()
# Async version with tenacity library (production-grade)
from tenacity import (
retry,
stop_after_attempt,
wait_exponential,
wait_jitter,
retry_if_exception_type,
)
@retry(
retry=retry_if_exception_type((RateLimitError, aiohttp.ClientError)),
stop=stop_after_attempt(5),
wait=wait_exponential(multiplier=1, min=1, max=60) + wait_jitter(max=2),
)
async def charge_payment_async(amount: float, card_token: str) -> dict:
async with aiohttp.ClientSession() as session:
async with session.post(
"https://api.payment.com/charge",
json={"amount": amount, "token": card_token},
) as resp:
if resp.status == 429:
raise RateLimitError("Rate limited")
resp.raise_for_status()
return await resp.json()
What You Learned
- Exponential backoff with jitter
- Building a retry decorator
- Using tenacity for async retries
Add More Questions to This Guide
Know a question that should be here? Share it and help the community!
Open Google Form