Skip to content

429 Too Many Requests through a proxy: find the limit

A 429 is a rate limit: something counted your requests and asked you to slow down, often with a Retry-After header that says for how long. Through a proxy it is almost always the website, after the tunnel opened. Find what the limit counts (the IP, a cookie, an API key or an account), then send fewer requests, not the same requests from more addresses.

Reviewed by John Hale. Last updated .

What it looks like

The website's response
> GET /search?q=proxy HTTP/2
< HTTP/2 429
< retry-after: 30
< content-type: text/html

Python requests, after raise_for_status()
requests.exceptions.HTTPError: 429 Client Error: Too Many Requests for url: https://example.com/search?q=proxy

What does 429 Too Many Requests mean?

RFC 6585 defines 429 as the user having sent too many requests in a given amount of time. The server may add Retry-After, either as seconds (Retry-After: 30) or as a date. It does not have to say how it counts.

Limits count different things. Some count requests per IP address, some per session cookie or API key, and some per account across every address. Others limit concurrency, so 20 parallel requests trip a limit that 20 requests one after another do not.

A proxy provider can enforce its own rate or connection limits as well. Those usually arrive as the answer to CONNECT or as the provider's own error, so curl -v tells the two apart.

What causes a 429 through a proxy?

Most likely first.

  1. Too many requests per second to one site. Scraping and monitoring jobs usually start at full speed. A burst at start-up, or parallel workers fetching the same pages, is the most common trigger.
  2. Retries without backoff. A client that retries every failure at once raises its rate exactly when the site is asking it to slow down, and can turn a 30-second limit into a long one.
  3. One sticky IP carries all the traffic. A sticky session is the right choice for logins and carts, but it puts every request from that worker on one address.
  4. The limit follows an account, cookie or API key. If every worker sends the same session cookie, token or API key, the site counts them together whatever address each one uses.
  5. Other traffic on the same address. Residential and mobile exits are shared networks: a household or carrier address can carry other traffic to the same site, which counts against a per-IP limit.

How do you find which limit you hit?

  1. Read the headers that came with the 429

    Retry-After gives the wait. RateLimit or X-RateLimit headers, where a site sends them, show the quota left and the window.

    Rate-limit headers
    curl -s -D - -o /dev/null -x http://HOST:PORT -U "USER:PASS" https://TARGET/ \
      | grep -iE "^(HTTP|retry-after|x-ratelimit|ratelimit)"

  2. Confirm who sent it

    A 429 after 200 Connection established is the site's. A 429 instead of the tunnel is the proxy's.

    Proxy's answer, then the site's
    curl -v -x http://HOST:PORT -U "USER:PASS" https://TARGET/ -o /dev/null 2>&1 \
      | grep -iE "^(> CONNECT|< HTTP|< x-proxy-reason|\* CONNECT)"

  3. Measure the rate that triggers it

    Run a slow loop and note when the first 429 appears. That tells you more about the limit than any guess.

    One request every 2 seconds
    for i in $(seq 1 30); do
      curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" \
        -x http://HOST:PORT -U "USER:PASS" https://TARGET/
      sleep 2
    done

  4. Find what the limit follows

    Send the same request from two sticky sessions without cookies, then with the same cookie. If the second exit is limited only when it shares the cookie or token, the limit follows the session, not the IP.

How do you fix a 429?

  • Honor Retry-After and back off

    Wait at least the Retry-After time before the next request to that site. Without one, start at a second or two and double the wait after each further 429, with random jitter and a cap on retries.

    Python requests
    import random, time
    import requests
    
    proxy = "http://USER:PASS@HOST:PORT"
    session = requests.Session()
    session.proxies = {"http": proxy, "https": proxy}
    
    def get(url, tries=5):
        for attempt in range(tries):
            r = session.get(url, timeout=30)
            if r.status_code != 429:
                return r
            wait = r.headers.get("Retry-After", "")
            delay = int(wait) if wait.isdigit() else 2 ** attempt
            time.sleep(delay + random.uniform(0, 1))
        return r

  • The same in Node.js

    Release the body of each refused response before waiting, so the connection goes back to the pool.

    Node.js with undici
    import { fetch, ProxyAgent } from "undici";
    
    const dispatcher = new ProxyAgent("http://USER:PASS@HOST:PORT");
    
    async function get(url, tries = 5) {
      let res;
      for (let attempt = 0; attempt < tries; attempt++) {
        res = await fetch(url, { dispatcher });
        if (res.status !== 429) return res;
        await res.body?.cancel();
        const seconds = Number(res.headers.get("retry-after"));
        const delay = seconds > 0 ? seconds * 1000 : 2 ** attempt * 1000;
        await new Promise((resolve) => setTimeout(resolve, delay + Math.random() * 1000));
      }
      return res;
    }

  • Send less

    Lower concurrency per site, start workers gradually instead of all at once, cache pages you already have, and use the site's API where it offers one: API limits are documented. Spreading one job across more addresses does nothing for limits on cookies, keys or accounts, and working around a limit the site has stated can breach its terms.

If you use TrueProxies

The gateway never answers 429. When it refuses a connection itself, it answers 407 with the reason in X-Proxy-Reason, so curl -v tells its refusals apart from a website's 429. Reusing connections with keep-alive or a connection pool avoids opening more connections than a job needs.

So a 429 inside a tunnel is the website's. Rotating sessions request a new IP for each connection, and the same IP can appear again. That spreads a per-IP count but does nothing for limits on cookies, keys or accounts.

If nearly every connection from a service fails at its destination for two minutes, the gateway pauses new connections briefly and says so in X-Proxy-Reason, with the counts and the wait. Slow the client down; the pause ends on its own.

Test the proxy from outside your network

The free proxy checker connects from our servers and reports the failed stage as a code, so you can tell a proxy problem from one on your computer or network.

Run the proxy checker

Questions about 429 Too Many Requests through a proxy

Does a 429 mean my proxy is bad?

No. It means a limit was reached. Through an open tunnel the limit is the website's, whichever proxy you use.

How long should I wait after a 429?

The Retry-After value, when there is one. Without it, start at a second or two and double the wait after each further 429, up to a cap.

Why do I get a 429 on the very first request?

The exit's address may already be over the site's per-IP limit from other traffic, or the site limits whole networks. Try another exit and a slower start.

Can the proxy send a 429 instead of the website?

Some providers use 429 for their own plan limits, as the answer to CONNECT. curl -v shows it before any 200 Connection established line.