How to Handle Retry-After Header in Python

Parse the Retry-After header from rate-limited API responses and implement retry logic with proper delays in Python.

Medium Python 3.9+ Aug 9, 2026 API design & gRPC 14 views 0 copies

Python code

62 lines
Python 3.9+
```python
import time
from datetime import datetime, timedelta


class RetryAfterHandler:
    def __init__(self, max_retries=3):
        self.max_retries = max_retries

    def get_retry_after_seconds(self, response_headers):
        retry_after_value = response_headers.get("Retry-After")
        if retry_after_value is None:
            return None

        try:
            return int(retry_after_value)
        except ValueError:
            try:
                retry_time = datetime.fromisoformat(retry_after_value)
                return max(0, int((retry_time - datetime.now()).total_seconds()))
            except ValueError:
                return None

    def retry_with_delay(self, request_func, response_headers):
        for attempt in range(self.max_retries):
            result = request_func()
            if not result.get("rate_limited"):
                return result

            seconds = self.get_retry_after_seconds(response_headers)
            if seconds is None:
                seconds = 5  # default delay if header is invalid

            print(f"Rate limited (attempt {attempt + 1}/{self.max_retries}), waiting {seconds}s...")
            time.sleep(seconds)

        return {"error": "Max retries exceeded"}


def mock_rate_limited_request():
    """Simulates a rate-limited response"""
    return {"rate_limited": True, "data": None}


def mock_success_request():
    """Simulates a successful response after a delay"""
    return {"rate_limited": False, "data": "ok"}


if __name__ == "__main__":
    handler = RetryAfterHandler(max_retries=2)

    # Example 1: numeric Retry-After header
    headers = {"Retry-After": "2"}
    result = handler.retry_with_delay(mock_rate_limited_request, headers)
    print(f"Result: {result}")

    # Example 2: HTTP date format Retry-After header
    future_time = datetime.now() + timedelta(seconds=3)
    headers = {"Retry-After": future_time.isoformat()}
    result = handler.retry_with_delay(mock_success_request, headers)
    print(f"Result: {result}")

Output

stdout
Rate limited (attempt 1/2), waiting 2s...
Rate limited (attempt 2/2), waiting 2s...
Result: {'error': 'Max retries exceeded'}
Result: {'rate_limited': False, 'data': 'ok'}

How it works

The get_retry_after_seconds method first tries to parse the header as an integer number of seconds, which is the most common format. If that fails, it attempts to parse it as an ISO 8601 date-time string and calculates the delay as the difference from now. The retry_with_delay method wraps a request function and automatically retries when rate_limited is true, using a sensible default of 5 seconds when the header is missing or invalid. This pattern is essential for building resilient clients that respect server-side rate limits without hammering the API.

Common mistakes

  • Assuming Retry-After is always a number — it can also be an HTTP-date string
  • Forgetting to handle negative time differences when the date is in the past
  • Not having a default delay fallback for invalid or missing headers

Variations

  1. Use `email.utils.parsedate_to_datetime` for full RFC 7231 HTTP-date support
  2. Extract the retry logic into a decorator for reuse across multiple endpoints

Real-world use cases

  • Building an API client that respects 429 rate-limit responses from services like Twitter or GitHub.
  • Implementing a web scraper that pauses politely between requests based on Retry-After headers.
  • Creating a background worker that processes rate-limited queue messages with exponential backoff.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from API design & gRPC

Related tutorials and quizzes for this topic.