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.

2 min read ~15 min to complete
Steps
1 Services Used
~15 min Duration
Intermediate Difficulty
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()
Services Used
Python
Prerequisites
  • Python 3.10+
  • OOP concepts
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