Proxy error · 429

429 Too Many Requests

A 429 is the site asking you to slow down, often with a Retry-After header saying how long to wait. It comes from the site or its API gateway, and the fix is nearly always pacing.
Whose fault is it?

The target, reacting to your pace. Your credentials are fine and the proxy delivered the request.

Usually from
The target, about your request rate
Try first
Read Retry-After and wait that long
With a proxy in the path

What a 429 means through a proxy

Rate limits count requests against a key: an IP address, an API key, a session cookie, an account, or some mix. Which key the site counts decides whether a proxy changes anything.

Over HTTPS the 429 you read comes from inside the tunnel, so the site sent it. If your tool reports a 429 as a proxy or tunnel error instead, it was the answer to CONNECT and came from the proxy.

Some sites send Retry-After as a number of seconds, some as an HTTP date, and some leave it out. A few use 403 or 503 for the same job.

The key question

Your credentials, the proxy, or the target?

The target, reacting to your pace. Your credentials are fine and the proxy delivered the request.

  • Your credentials

    Fine. The request got through the proxy and the site answered it.

  • The proxy

    Only in that one static IP collects every request against one address. Rotation spreads them, which helps with limits counted per IP and nothing else.

  • The target site

    The source, and it is telling you its limit.

How to tell

  • A Retry-After header on the 429, or X-RateLimit-Remaining style headers counting down on the responses before it.
  • The 429s start after a burst and stop when you pause. That is a rate limit doing its job.
  • A new residential address does not stop them. The limit is counted on your API key, cookie or account.
  • A 429 inside a ProxyError or TunnelError came from the proxy during CONNECT. Paste that one into the Discord.
Cheapest first

Fixes, in the order to try them

  1. Honour Retry-After

    Wait the number of seconds it gives, or until the date it gives. Retrying sooner tends to extend the block.

    Costs nothing
  2. Cap concurrency per domain

    Most 429s come from parallel workers all hitting one host. Limit how many requests run against the same domain at once; two to four is plenty for most sites.

    Costs nothing
  3. Back off exponentially, with jitter

    After a 429 with no Retry-After, wait 1, 2, 4, then 8 seconds plus a random fraction, so parallel workers do not retry in lockstep. Give up after a few tries and log the URL for later.

    Costs nothing
  4. Read the site’s own limits

    APIs publish their rate limits, and robots.txt may carry a Crawl-delay. Staying under a published limit costs nothing and keeps the pool usable for everyone on it.

    Costs nothing
  5. Stop fetching what you already have

    Cache pages between runs. Conditional requests with If-Modified-Since or If-None-Match let the site answer 304 with no body, which counts less against most limits and moves fewer bytes.

    Costs some time
  6. Spread a per-IP limit with rotating residential

    If the limit is per address and your overall pace is reasonable, residential sends each request from a different address. An ISP or datacenter IP is one address, and one static IP gets rate-limited eventually.

    Costs money
Per tool

See the real status and headers

Each sample reads the limit headers and waits the way the site asked, which is the whole fix most of the time.

curl
terminal
# the status and any rate-limit headers
curl -s -o /dev/null -D - -x http://USER:[email protected]:8000 https://api.example.com/items \
  | grep -iE '^(HTTP|retry-after|x-ratelimit)'

# --retry retries a 429 and waits as long as Retry-After says
curl --retry 3 --retry-max-time 120 -x http://USER:[email protected]:8000 https://api.example.com/items
Python Requests
polite.py
import random
import time
from email.utils import parsedate_to_datetime

import requests

PROXY = "http://USER:[email protected]:8000"


def pause_for(r, attempt):
    header = r.headers.get("Retry-After")
    if header is None:
        return 2 ** attempt + random.random()
    if header.isdigit():
        return int(header)
    return max(0, parsedate_to_datetime(header).timestamp() - time.time())


def get(url, tries=4):
    for attempt in range(tries):
        r = requests.get(url, proxies={"http": PROXY, "https": PROXY}, timeout=30)
        if r.status_code != 429:
            return r
        pause = pause_for(r, attempt)
        print(f"429 on {url}, waiting {pause:.1f}s")
        time.sleep(pause)
    return r
Scrapy
settings.py
ROBOTSTXT_OBEY = True
CONCURRENT_REQUESTS_PER_DOMAIN = 2
DOWNLOAD_DELAY = 1.0
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
RETRY_HTTP_CODES = [429, 500, 502, 503, 504]
RETRY_TIMES = 3

Scrapy’s retry middleware retries a 429 without reading Retry-After, so the delay and AutoThrottle settings are what keep you under the limit.

Playwright
paced.py
from playwright.sync_api import Error, sync_playwright

PROXY = {"server": "http://resi.proxymonkey.io:8000", "username": "USER", "password": "PASS"}

URLS = ["https://example.com/a", "https://example.com/b"]


def log_limits(response):
    if response.status == 429:
        print("429 on", response.url, "retry-after:", response.headers.get("retry-after"))


with sync_playwright() as p:
    browser = p.chromium.launch(proxy=PROXY)
    page = browser.new_page()
    page.on("response", log_limits)
    for url in URLS:
        page.goto(url)
        page.wait_for_timeout(2000)
    browser.close()

One page load is dozens of requests, and scripts or API calls behind the page can hit the limit before the HTML does. The listener catches all of them.

Node.js
paced.mjs
import { setTimeout as sleep } from "node:timers/promises";
import { fetch, ProxyAgent } from "undici";

const dispatcher = new ProxyAgent("http://USER:[email protected]:8000");

async function get(url, tries = 4) {
  for (let attempt = 0; attempt < tries; attempt++) {
    const res = await fetch(url, { dispatcher });
    if (res.status !== 429) return res;
    const after = Number(res.headers.get("retry-after"));
    const pause = after > 0 ? after * 1000 : 2 ** attempt * 1000 + Math.random() * 1000;
    await res.body?.cancel();
    await sleep(pause);
  }
  throw new Error("still 429 after " + tries + " tries: " + url);
}

The samples use the residential gateway, resi.proxymonkey.io:8000. For an ISP or datacenter IP, use USER:PASS@IP:PORT for the address you rented. Your dashboard lists the host and port for every order, and where it differs from this page, the dashboard is right.

Before you buy anything

Will a different proxy line fix it?

When switching helps

When the site limits per IP and your overall pace is polite, rotating residential spreads requests over many addresses. Our ISP page says so too: one static IP gets rate-limited eventually.

When it will not

Limits counted per API key, login, cookie or account ignore the IP. And using rotation to push a site harder than it can take is overloading a target, which the acceptable use policy does not allow.

The meter

Is a failed request billed?

We bill for request bytes and response bytes, including headers and protocol overhead on the tunnelled connection. Connections that fail before transferring data are not billed. Retries that you initiate are billed and appear as separate entries in your usage log.

From the metering section of our terms of service.

A 429 is a response that travelled back through the proxy, and the terms bill request and response bytes with no exception for status. They also bill every retry you initiate as its own entry in the usage log, so a loop that ignores Retry-After costs money as well as time.

Residential is billed per GB of that traffic. ISP and datacenter addresses are charged per IP for their term, and where a plan includes a traffic allowance, traffic past it is billed per GB under the same rule. The usage log in your dashboard has one row per request with bytes in, bytes out and cost, so you can look up the failed request yourself.

429, asked often

Questions people ask about a 429

Will rotating proxies stop 429 errors?

Only when the site limits per IP address. Many limits count per API key, login or cookie, and a new address changes nothing there. Even when rotation helps, keep your total rate to something the site can handle.

Is a 429 the proxy limiting me?

A 429 you read as a response over HTTPS came from the site, because the proxy cannot write inside the tunnel. If your tool reports the 429 as a proxy or tunnel error, it came from the gateway during CONNECT. Paste that one into the Discord.

How long should I wait after a 429?

As long as Retry-After says. Without the header, back off exponentially from a second or two and add a little randomness so parallel workers do not all retry at the same moment.

Do retries after a 429 cost traffic?

Yes. The terms say retries you initiate are billed and appear as separate entries in your usage log. A retry sent before Retry-After has passed usually gets another 429, so it is the most wasteful kind.

The community layer

Still stuck on a 429?

Paste your error in the Discord: the full message plus the command or the few lines that set up the proxy, with the password taken out. Someone there has seen it before.

Join the Discord

4,200+monkeys in the Discord

  • Help from humans

    Post your error, get an answer. Usually in minutes, usually from someone who has hit the same wall.

  • A status bot that tells on us

    Pool health, incidents and maintenance posted automatically. Including the bad days.

  • Deals and free traffic

    Bonus GB drops, early access to new pools, and the occasional giveaway for a good bug report.

Join the Discord4,200+ monkeys, free to lurk