How to Handle 429 (Rate Limit) Errors in a REST API in Python
When an integration starts failing with the HTTP status code 429, the API is telling you that you are sending too many requests. Knowing how to handle this error with retries and exponential backoff is what separates a fragile script from a reliable data integration. Let's see how to do it in Python, in a simple, step-by-step way.
Prerequisites
- Python 3.8 or later installed.
- The
requestslibrary (install it withpip install requests). - Basic knowledge of REST API requests (GET and headers).
- An endpoint to test with — you can use the public
httpbin.orgservice.
Step 1: Understand the 429 error and the Retry-After header
The 429 Too Many Requests status code means you have exceeded the request limit (rate limit) the API allows within a given time window. This limit exists to protect the service from overload and to guarantee fair access for every client. Many APIs respond with a Retry-After header that tells you how many seconds to wait before trying again.
If the response includes the Retry-After header, always respect it. It is the API's official signal about when you may try again.
Step 2: Detect the 429 in a simple request
Start with a normal request and check the status code. This lets you see when the API returns 429 before you build the retry logic.
import requests
resposta = requests.get("https://httpbin.org/status/429", timeout=10)
if resposta.status_code == 429:
print("Recebi 429 — a API pediu para abrandar.")
else:
print("Tudo certo:", resposta.status_code)
Step 3: Retry with exponential backoff
Instead of always retrying with the same interval, increase the wait time after each failure: 1s, 2s, 4s, 8s... This is called exponential backoff and it avoids hammering the API. If the Retry-After header is present, we use its value; otherwise, we compute the wait based on the attempt number.
import time
import requests
def get_com_retry(url, max_tentativas=5):
for tentativa in range(max_tentativas):
resposta = requests.get(url, timeout=10)
if resposta.status_code != 429:
resposta.raise_for_status()
return resposta
espera = int(resposta.headers.get("Retry-After", 2 ** tentativa))
print(f"429 recebido. A aguardar {espera}s (tentativa {tentativa + 1})")
time.sleep(espera)
raise RuntimeError("Limite de tentativas excedido")
The loop repeats the request until it succeeds or runs out of attempts. The function returns the response as soon as the API responds well and raises a clear error if it gives up. The timeout makes sure a request never hangs forever.
Step 4: The robust approach with Session and Retry
For production, requests integrates with urllib3's Retry mechanism, which handles retries automatically for every request in a session. It is less code and also covers server errors (500, 502, 503 and 504).
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
retry = Retry(
total=5,
backoff_factor=1, # aumenta a espera a cada tentativa
status_forcelist=[429, 500, 502, 503, 504],
respect_retry_after_header=True,
)
sessao = requests.Session()
adaptador = HTTPAdapter(max_retries=retry)
sessao.mount("https://", adaptador)
sessao.mount("http://", adaptador)
resposta = sessao.get("https://httpbin.org/get", timeout=10)
print(resposta.status_code)
With respect_retry_after_header=True, the session automatically respects the Retry-After header sent by the API. The status_forcelist defines which status codes trigger a new attempt.
Verify the result
To confirm the logic works, point the function at https://httpbin.org/status/429, which always returns 429: you will see the wait messages appear and the time between attempts grow. Then switch to https://httpbin.org/get, which responds 200, and confirm the function returns the response on the first try. If you enable logging, you should see at most the number of attempts you defined.
Conclusion
In just a few lines you went from a script that breaks on the first 429 to an integration that slows down politely and recovers on its own. The natural next step is to combine this logic with the API's pagination and authentication, giving you a complete, fault-tolerant client. What value of max_tentativas makes sense for the API you work with — and do you already know the request limit it enforces?