No KYC — no ID, no selfie, no documents. Pay in crypto: BTC, USDT, USDC, ETH, SOL, LTC
Language: English
Get started

Guide · responsible crawling · 2026

403, 429 and CAPTCHAs: respond, don't evade

A 403, a 429 or a CAPTCHA is a site talking to your crawler. The right answer is almost always to slow down, read the rules, or ask; the wrong one is to disguise the crawler. Here is how to build the right answer into code.

Last updated: · 6 min read

Quick answer

How should a scraper handle 403, 429 and CAPTCHA responses?

Treat them as signals, not obstacles. On a 429, wait for Retry-After and lower your rate on that host. On a 403, retry once after a pause, then stop and check the site's rules. On a CAPTCHA, stop that host: do not solve it automatically. CrawlRelay residential proxies, from $0.50/GB, spread load and set location; they are not a way around a site's decision. No KYC. Pay in crypto.

The three signals at a glance

429 Too Many RequestsYou are too fast. Honour Retry-After, back off, lower per-host concurrency
403 ForbiddenThis request is refused. One retry after a pause, then stop and review
CAPTCHA or challenge pageThe site wants to know if you are human. Stop the host and slow down
Standard for crawl rulesrobots.txt, RFC 9309; Crawl-delay is a common extension
What we do not supportCAPTCHA solving, stealth plugins, fingerprint spoofing

What is each signal telling my crawler?

  • 429 is the politest: the content is available, but not at your pace. RFC 6585 defines it, often with a Retry-After header that says when to return.
  • 403 is a refusal of this request. It can be fixable (wrong country, missing headers, too fast) or final (the site does not allow automated access to that content).
  • A CAPTCHA or a challenge page, often served with a 403 or 503 status, means the site's bot management doubts your traffic. It is an escalation: the step after ignored 429s.

All three are rising signals. Sites that see their 429s ignored escalate to 403s and challenges, then to long blocks on whole IP ranges, which hurts every user of those ranges.

What should my crawler do for each signal?

SignalFirst responseIf it continuesStop when
429Wait for Retry-After or back off exponentially with jitterHalve concurrency on that hostRetry-After keeps growing, or 429s persist at one request at a time
403One retry after a pause, same countryCompare with a browser from the same country; check robots.txt and termsA second 403 on the same URL pattern
CAPTCHA / challengePause the host for several minutesResume at a much lower rateThe challenge returns at a low rate: the site does not want automated access
503 with Retry-AfterWait as askedPause the hostMost requests fail for more than a few minutes

How do I build a crawler that responds correctly?

Put the rules in the fetcher, not in each spider. This one sends one request at a time per host, honours robots.txt and its Crawl-delay, waits for Retry-After, and stops asking a host after three refusals in a row. It treats a 4xx robots.txt as permission and a 5xx as a full disallow, as RFC 9309 specifies.

Python: a polite fetcher
import time
import urllib.robotparser
from urllib.parse import urlsplit

import requests

PROXY = "http://USERNAME-country-us:[email protected]:7777"
USER_AGENT = "ExampleCrawler/1.0 (+https://example.com/crawler-info)"


class PoliteFetcher:
    """One request at a time per host; robots.txt, Crawl-delay and Retry-After honoured."""

    def __init__(self, min_interval=1.0, max_refusals=3):
        self.min_interval, self.max_refusals = min_interval, max_refusals
        self.http = requests.Session()
        self.http.proxies = {"http": PROXY, "https": PROXY}
        self.http.headers["User-Agent"] = USER_AGENT
        self.robots, self.next_time, self.refusals = {}, {}, {}

    def _robots(self, root):
        if root not in self.robots:
            rp = urllib.robotparser.RobotFileParser(root + "/robots.txt")
            try:
                r = self.http.get(root + "/robots.txt", timeout=(10, 30))
                if r.status_code >= 500:
                    rp.parse(["User-agent: *", "Disallow: /"])  # unreachable: assume disallow
                else:
                    rp.parse(r.text.splitlines() if r.status_code == 200 else [])  # 4xx: allowed
            except requests.RequestException:
                rp.parse(["User-agent: *", "Disallow: /"])
            self.robots[root] = rp
        return self.robots[root]

    def get(self, url):
        scheme, host = urlsplit(url)[:2]
        if self.refusals.get(host, 0) >= self.max_refusals:
            return None  # this host said no: stop asking
        rp = self._robots("%s://%s" % (scheme, host))
        if not rp.can_fetch(USER_AGENT, url):
            return None
        delay = max(self.min_interval, rp.crawl_delay(USER_AGENT) or 0)
        wait = self.next_time.get(host, 0) - time.monotonic()
        if wait > 0:
            time.sleep(wait)
        r = self.http.get(url, timeout=(10, 30))
        pause = delay
        if r.status_code in (429, 503):
            retry_after = r.headers.get("Retry-After", "")
            pause = max(delay, int(retry_after) if retry_after.isdigit() else 60)
        refused = r.status_code in (403, 429) or "captcha" in r.text[:5000].lower()
        self.refusals[host] = self.refusals.get(host, 0) + 1 if refused else 0
        self.next_time[host] = time.monotonic() + pause
        return r


fetcher = PoliteFetcher()
resp = fetcher.get("https://example.com/")
print(resp.status_code if resp is not None else "skipped")

What fixes a 403 legitimately?

Several 403 causes are on your side, and fixing them is fair:

  • The wrong country. Many sites serve one region. Target the country with -country-<code>.
  • Incomplete headers. Send Accept, Accept-Language and an honest user agent with a contact URL. A browser-like user agent on a plain HTTP client is inconsistent and often refused.
  • Your own pace. A 403 that starts after N requests is a rate problem; slow down.

What is not a fix: rotating user agents to look like many different people, spoofing browser fingerprints, or cycling IPs to get around a block the site set on purpose. If a site filters hosting ranges to keep automated traffic out, a residential IP does not make that access permitted.

Why not just solve the CAPTCHAs?

Because a CAPTCHA is the site asking a question you would answer falsely. Automated solving turns a polite crawler into a tool built to defeat an access control, which raises your legal risk: the 2026 US cases over search scraping, Google v. SerpApi and Reddit v. SerpApi and others, turn on claims that defendants circumvented an anti-bot system under the DMCA's anti-circumvention rule. Courts have not settled the question, but the risk is real.

CrawlRelay does not support CAPTCHA solving or bot-detection evasion, and its Acceptable Use Policy forbids them through the gateway. If you need data behind a challenge, ask the site for access, use its API, or license the data.

When should I contact the site instead?

When you need more than its defaults allow. Many sites offer an API, a data feed, a partner programme or simply a higher limit for a crawler that identifies itself and explains its purpose. Put a contact URL in your user agent, keep a page that describes your crawler and how to opt out, and answer abuse reports quickly. A crawler that can be reached gets warnings before blocks.

403, 429 and CAPTCHAs: questions

Do residential proxies stop CAPTCHAs?

Do not count on it. A residential exit changes which IP a site sees, not what your crawler does: CAPTCHAs caused by your pace or behaviour remain, and they are meant to. If challenges continue at a low request rate, the site does not want automated access to that content.

How long should I wait after a 429 without Retry-After?

Start at a few seconds and double the wait after each new 429, with a random extra and a cap of about a minute. When requests succeed again, resume at a lower rate than the one that caused the 429, and raise it slowly while the error rate stays flat.

Is rotating IPs to avoid rate limits allowed?

Rotation spreads load across exits and lets you choose locations, which is normal. Using rotation to stay under a per-IP limit while keeping a rate the site refused is evasion. Most terms of use forbid it, and so does our Acceptable Use Policy.

What user agent should my scraper send?

Your own: a name, a version and a URL that explains the crawler and how to contact you, such as ExampleCrawler/1.0 (+https://example.com/crawler-info). Do not use the names of other crawlers such as Googlebot or GPTBot.

Sources (4)
  1. RFC 9309: Robots Exclusion Protocol — IETF (2022-09)
  2. RFC 6585: Additional HTTP Status Codes (429) — IETF (2012-04)
  3. Reddit v. SerpApi LLC — Loeb & Loeb (2026-08)
  4. Google v. SerpApi: the court granted our motion to dismiss — SerpApi (2026-07-21)

Residential proxies for polite crawlers

$1.50 for 1 GB, $0.50/GB from 50 GB.

From $0.50/GB · no KYC

Get started