{"id":25351,"date":"2026-08-28T08:42:47","date_gmt":"2026-08-28T08:42:47","guid":{"rendered":"https:\/\/capskip.com\/?p=25351"},"modified":"2026-08-28T08:42:47","modified_gmt":"2026-08-28T08:42:47","slug":"http-429-too-many-requests","status":"publish","type":"post","link":"https:\/\/capskip.com\/zh\/http-429-too-many-requests\/","title":{"rendered":"\u722c\u866b\u9047\u5230 HTTP 429 Too Many Requests \u600e\u4e48\u4fee"},"content":{"rendered":"<p>HTTP 429 Too Many Requests means slow down, not go away. The server is telling you it still wants your traffic at a lower rate, and it usually tells you exactly how long to wait. So the fix is almost never a proxy or a new user agent. It is reading one header, sleeping properly, and capping how many requests you have in flight at once. This post covers all three, then shows you how to tell a real rate limit apart from a bot block wearing the same status code, because the two need opposite responses.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">What you need<\/h2>\n<ul>\n<li>Python 3.10 or newer, with the requests library, for the examples. The logic ports directly to any HTTP client.<\/li>\n<li>A terminal, so you can look at response headers before you write any retry code.<\/li>\n<li>CapSkip running for the last section only, either in Local mode on the loopback address or in Server mode on a machine your workers can reach. Both are covered under <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">connection settings<\/a>, so pick one before you start.<\/li>\n<\/ul>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 1: read the response before you retry it<\/h2>\n<p>Most 429 handling is written blind, which is why it does not work. Look at the actual response first. The status code arrives with headers that tell you what the limit is and when it resets, and different services use different ones.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"bash\" class=\"EnlighterJSRAW\"># No install needed. Dump headers, throw the body away.\ncurl -sS -o \/dev\/null -D - &quot;https:\/\/example.com\/api\/items?page=2&quot;\n\n# Look for these, in this order of usefulness:\n#   Retry-After: 30           seconds, or an HTTP date\n#   RateLimit-Reset: 1724500000\n#   X-RateLimit-Remaining: 0\n#   RateLimit-Limit: 100<\/pre>\n<\/div>\n<p>The one that matters is Retry-After. It is defined for exactly this situation and it comes in two forms: a number of seconds to wait, or an absolute HTTP date. Both are legal, both appear in the wild, and code that assumes the number will break on the sites that send the date. <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Reference\/Headers\/Retry-After\" rel=\"noopener nofollow\" target=\"_blank\">MDN documents both forms<\/a>, and its reference page for <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Reference\/Status\/429\" rel=\"noopener nofollow\" target=\"_blank\">the 429 status code<\/a> is worth two minutes of your time as well.<\/p>\n<p>Parse it defensively, honour it when it is there, and fall back to your own schedule when it is not:<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install requests\nfrom email.utils import parsedate_to_datetime\nfrom datetime import datetime, timezone\n\ndef retry_delay(response, fallback):\n    &quot;&quot;&quot;Seconds to wait, from Retry-After if the server sent one.&quot;&quot;&quot;\n    raw = response.headers.get(&quot;Retry-After&quot;)\n    if not raw:\n        return fallback\n    try:\n        return max(0.0, float(raw))          # the delay-seconds form\n    except ValueError:\n        pass\n    try:\n        when = parsedate_to_datetime(raw)    # the HTTP-date form\n        return max(0.0, (when - datetime.now(timezone.utc)).total_seconds())\n    except (TypeError, ValueError):\n        return fallback<\/pre>\n<\/div>\n<p>Cap whatever comes back. A server that asks for 3600 seconds is telling you to stop for the hour, and a worker that obediently sleeps that long inside a request handler will look like a hang to everything above it. Take the smaller of the header value and your own ceiling, then decide separately whether to shelve the job.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 2: back off exponentially, with jitter<\/h2>\n<p>When there is no Retry-After to follow, double the wait each time and add randomness. The doubling is what stops you hammering a service that is already struggling. The randomness is what stops twenty of your own workers, which all hit the limit in the same second, from retrying in the same second forever.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install requests\nimport random, time, requests\n\ndef get_with_backoff(url, attempts=5, base=1.0, ceiling=60.0):\n    for attempt in range(attempts):\n        response = requests.get(url, timeout=30)\n        if response.status_code != 429:\n            return response\n        # Full jitter: sleep somewhere in [0, base * 2 ** attempt].\n        window = min(ceiling, base * (2 ** attempt))\n        delay = retry_delay(response, random.uniform(0, window))\n        time.sleep(min(delay, ceiling))\n    raise RuntimeError(f&quot;still rate limited after {attempts} attempts&quot;)\n\n# Waits land near 0-1s, 0-2s, 0-4s, 0-8s, 0-16s unless the\n# server named a delay, in which case that wins.<\/pre>\n<\/div>\n<p>Full jitter, meaning a random value between zero and the window rather than the window plus a small wobble, is the variant that de-synchronises a fleet fastest. If you would rather not write it yourself, urllib3 has this built in, but it needs two arguments set explicitly to be useful:<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install requests\nimport requests\nfrom requests.adapters import HTTPAdapter\nfrom urllib3.util import Retry\n\nretry = Retry(\n    total=5,\n    # status_forcelist defaults to none, so 429 is NOT retried\n    # unless you list it here yourself. This is the usual bug.\n    status_forcelist=[429, 500, 502, 503, 504],\n    backoff_factor=1,        # 1 * 2 ** previous_retries seconds\n    backoff_jitter=1.0,      # urllib3 2.x only\n    allowed_methods=[&quot;GET&quot;, &quot;HEAD&quot;],\n)\n\nsession = requests.Session()\nsession.mount(&quot;https:\/\/&quot;, HTTPAdapter(max_retries=retry))<\/pre>\n<\/div>\n<p>Two details are worth knowing about that class. It honours Retry-After for you already, because 429 is one of the three status codes in its RETRY_AFTER_STATUS_CODES set alongside 413 and 503, and respect_retry_after_header defaults to true. But status_forcelist defaults to nothing at all, so a fresh Retry object does not retry a 429 until you name it. People assume the opposite and then wonder why the adapter did nothing.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Step 3: cap concurrency instead of retrying harder<\/h2>\n<p>Retry logic treats the symptom. If you are getting 429s steadily rather than in bursts, you are simply asking for more than you are allowed, and the fix is to send less. A semaphore plus a floor on the gap between requests fixes more rate limiting than any backoff curve.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># Standard library only.\nimport asyncio\n\n# Six in flight is a sane starting point for an unknown API.\ngate = asyncio.Semaphore(6)\nMIN_GAP = 0.2          # seconds between starts, per worker\n\nasync def fetch(client, url):\n    async with gate:\n        response = await client.get(url)\n        await asyncio.sleep(MIN_GAP)\n        return response\n\n# Tune down on the first 429, and stay there for a while.\n# Tuning back up too eagerly just rediscovers the limit.<\/pre>\n<\/div>\n<p>The instinct after a 429 is to spread the same load across more IPs. That works for some targets and it is a different decision with its own tradeoffs, which we went through separately in the guide to <a href=\"https:\/\/capskip.com\/captcha-proxy-rotation\/\">CAPTCHA proxy rotation<\/a>. Do it as a capacity choice, not as a way to avoid reading a header.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">A 429 is not a 403, and neither is a challenge<\/h2>\n<p>Here is where 429 handling goes wrong most expensively. Rate limiting and bot detection are different systems that sometimes share a status code, and they want opposite things from you. An HTTP 429 Too Many Requests from a rate limiter is a scheduling instruction, while the same code from an anti-bot edge is a refusal. Backing off politely against a bot block wastes an hour. Retrying hard against a real rate limit gets your IP banned.<\/p>\n<table>\n<thead>\n<tr>\n<th>What you got<\/th>\n<th>What it usually means<\/th>\n<th>What actually helps<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>429 with a Retry-After header<\/td>\n<td>A real, documented rate limit<\/td>\n<td>Wait exactly that long, then lower your rate<\/td>\n<\/tr>\n<tr>\n<td>429 with no headers and an HTML body<\/td>\n<td>An edge or anti-bot layer, not the API<\/td>\n<td>Treat it as a block, not a limit<\/td>\n<\/tr>\n<tr>\n<td>403 arriving instantly<\/td>\n<td>Fingerprint, TLS or IP reputation<\/td>\n<td>Fix the client, since waiting changes nothing<\/td>\n<\/tr>\n<tr>\n<td>503 with Retry-After<\/td>\n<td>Overloaded or in maintenance<\/td>\n<td>Same backoff path as 429<\/td>\n<\/tr>\n<tr>\n<td>200 carrying a challenge page<\/td>\n<td>You have been scored and interrupted<\/td>\n<td>Solve the challenge and carry on<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>That last row catches people out, because nothing failed. The request returned 200 and the body is a challenge page rather than your data, so a status-code-only retry loop will happily hammer it forever. Check for the marker you expect in the body, not just the status line. If a challenge is what you found, backing off is not the answer and solving it is.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Keep your solve loop off the same rake<\/h2>\n<p>The polling loop that waits for a CAPTCHA answer is itself a retry loop, and hand-rolled ones make exactly the mistakes above. Two things about CapSkip make this easier than it is against a metered service. It runs on your own hardware, so there is no per-solve quota to exhaust and no rate limit of its own to trip. And the SDKs already back off for you: polling starts at a quarter of a second and grows up to pollingInterval, which is a ceiling rather than a fixed gap.<\/p>\n<div data-no-translation>\n<pre data-enlighter-language=\"python\" class=\"EnlighterJSRAW\"># pip install capskip\nfrom capskip import CapSkip, NetworkException, TimeoutException\n\n# Local mode. In Server mode, host is the solver box's address.\nsolver = CapSkip(host=&quot;127.0.0.1&quot;, port=8080, pollingInterval=2)\n\ntry:\n    result = solver.recaptcha(\n        sitekey=&quot;YOUR_SITEKEY&quot;,\n        url=&quot;https:\/\/example.com\/page-with-recaptcha&quot;,\n    )\n    print(result[&quot;code&quot;][:24])   # token, submit it with the form\nexcept TimeoutException:\n    # Polling ran past recaptchaTimeout, 300 seconds by default.\n    print(&quot;gave up waiting, try again or lower the timeout&quot;)\nexcept NetworkException:\n    # The solver is not reachable on that host and port.\n    print(&quot;check CapSkip is running and the mode you set&quot;)<\/pre>\n<\/div>\n<p>Lowering pollingInterval makes an answer arrive sooner and costs you nothing, which is a choice you do not really have on a billed API. If you are polling the raw HTTP endpoints instead of using an SDK, the recommended delays per CAPTCHA type are listed in the <a href=\"https:\/\/capskip.com\/api-docs\/\">API documentation<\/a>, along with the pending response you will get while a solve is still in progress.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">Running the solver on a server instead<\/h2>\n<p>Rate limiting is usually a problem for a fleet rather than one script, and a fleet does not share a loopback address. The connection settings cover both cases:<\/p>\n<table>\n<thead>\n<tr>\n<th>Mode<\/th>\n<th>Listens on<\/th>\n<th>Use it when<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Local<\/td>\n<td>127.0.0.1, that device only<\/td>\n<td>Your scraper and the solver run on one machine<\/td>\n<\/tr>\n<tr>\n<td>Server<\/td>\n<td>Your network address or public IP<\/td>\n<td>Workers, containers, a VPS or a hosted platform call in over the API<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Point the SDK host at the solver machine and nothing else in your code changes, so ten workers can share one instance. A static public IP is recommended when the callers sit outside your own network. The details are under <a href=\"https:\/\/capskip.com\/setup-guide\/#connection-settings\">connection settings<\/a>, and Server mode is still your hardware and still unmetered: it moves where the solver runs, not who owns it.<\/p>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">FAQ<\/h2>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Should I always obey Retry-After?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Honour it, but cap it. It is the most reliable signal you will get about when the window reopens, so ignoring it means guessing worse than the server already told you. A value of an hour is a different decision though: park the job and come back rather than holding a worker asleep, because everything upstream will read that as a hang.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">How do I tell a rate limit from a bot block?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">Look at what came with the status code. A real limit is machine readable: a Retry-After or a RateLimit header, a small JSON body, and consistent behaviour when you wait. A bot block sends an HTML page, no timing information, and often the same response no matter how long you leave it. The second one wants a different client, not a longer sleep.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">Does the solver ever return a 429?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">No. CapSkip runs on hardware you control with no per-solve quota, so there is no billing window to exhaust and no upstream limit to hit. If a solve call fails you will get a network error because the daemon is unreachable, or a timeout because polling ran past its ceiling. Both mean something local, so check the host and port and the mode you configured.<\/p>\n<\/details>\n<details style=\"border:1px solid #e2e5ee;border-radius:10px;padding:14px 18px;margin:0 0 12px;\">\n<summary style=\"cursor:pointer;\">\n<h3 style=\"font-size:1.15rem;line-height:1.4;display:inline;margin:0;\">My workers run on a hosted platform. Where does the solver go?<\/h3>\n<\/summary>\n<p style=\"margin:12px 0 0;\">On a machine of yours that the platform can reach, with the solver switched to Server mode. Hosted runners and managed automation platforms cannot see your loopback address, so bind the API to your network or public IP and point every worker at it. One instance serves the whole fleet and no tunnel is needed.<\/p>\n<\/details>\n<h2 style=\"font-size:1.6rem;line-height:1.35;\">The shortest version<\/h2>\n<p>HTTP 429 Too Many Requests is a scheduling problem, so treat it like one. Read Retry-After and obey it up to a ceiling you choose. Fall back to exponential backoff with full jitter when the header is absent. Then lower your concurrency, because steady 429s are a capacity problem that no retry curve solves. And check the body before you retry at all, since a challenge page arrives with a perfectly healthy status code and wants solving rather than waiting. For that part an <a href=\"https:\/\/capskip.com\/\">unlimited captcha solver<\/a> running locally is the piece that fits, and the notes on running a <a href=\"https:\/\/capskip.com\/captcha-solver-for-web-scraping\/\">CAPTCHA solver for web scraping<\/a> cover how it slots into a worker pool.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>429 \u662f\u670d\u52a1\u5668\u80fd\u5bf9\u4f60\u8bf4\u7684\u6700\u53cb\u5584\u7684\u8bdd\uff1a\u6162\u4e00\u70b9\uff0c\u7136\u540e\u518d\u56de\u6765\u3002\u672c\u6587\u8bb2\u89e3\u600e\u4e48\u8bfb\u61c2\u5b83\u3001\u600e\u4e48\u6b63\u786e\u9000\u907f\uff0c\u4ee5\u53ca\u600e\u4e48\u8ba4\u51fa\u4e00\u4e2a\u5047\u7684 429\u3002<\/p>","protected":false},"author":1,"featured_media":25350,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"HTTP 429 Too Many Requests: The Fix | CapSkip","rank_math_description":"HTTP 429 Too Many Requests means slow down, not stop. Read Retry-After, back off with jitter, cap concurrency, and learn when a 429 is really a bot block.","rank_math_focus_keyword":"http 429 too many requests","footnotes":""},"categories":[70],"tags":[],"class_list":["post-25351","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-captcha"],"_links":{"self":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25351","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/comments?post=25351"}],"version-history":[{"count":1,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25351\/revisions"}],"predecessor-version":[{"id":25435,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/posts\/25351\/revisions\/25435"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media\/25350"}],"wp:attachment":[{"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/media?parent=25351"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/categories?post=25351"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/capskip.com\/zh\/wp-json\/wp\/v2\/tags?post=25351"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}