zeitbach.com

Honoring the Retry-After header with Tenacity

Tenacity allows you to retry anything. That’s great, but this generality also means that it simply doesn’t know anything about HTTP headers or status codes.

To support the Retry-After HTTP header, we implement our own wait strategy wait_retry_after that we’ll pass to the Retrying controller. For the sake of simplicity, I retry on any status code >= 400 in the following snippet.

retrying = Retrying(
    retry=retry_if_result(lambda result: result.status_code >= 400),
    wait=wait_retry_after(
        wait_exponential(multiplier=1, min=4, max=10)
    ),
)

Here is the implementation.

class wait_retry_after(wait_base):
    def __init__(self, fallback: wait_base) -> None:
        self.fallback = fallback

    def __call__(self, retry_state: RetryCallState) -> float:
        if outcome := retry_state.outcome:
            response = outcome.result()
            if response.status_code in (429, 503) and (
            retry_after := response.headers.get("Retry-After")):
                return float(retry_after)
        return self.fallback(retry_state)

If we get a 429 or 503 status code and a Retry-After header on our response, this strategy returns the wait time indicated in the header, otherwise it executes a fallback strategy – in our case, the wait_exponential we passed above.

In order for our strategy to work, we need to make sure to set the result on the retry state after each retry attempt, so that we have access to the response in our custom strategy. If you implement some HTTP client lib, this may go into your private _request method that is called internally by each of your public methods.1

for attempt in retrying:
    with attempt:
        response = self.session.request(...)
    if (not attempt.retry_state.outcome or 
        not attempt.retry_state.outcome.failed):
        attempt.retry_state.set_result(response)

The user of your lib only needs to pass it a Retrying object to configure the retry behavior.


  1. I used the synchronous version here because it’s shorter, but you can do the same with async.↩︎