OmniScrapeOmniScrape
ProductsSolutionsGuidesDocs ↗PricingAbout
← All guides
Web Scraping Guides

Web Scraping Rate Limits: Handle HTTP 429 Correctly

Web scraping rate limits are a scheduling problem, not a signal to retry faster. An HTTP 429 response means the sender made too many requests within a limit chosen by the server. The correct response is to identify which layer applied the limit, pause the affected traffic, and resume at a lower controlled rate.

This guide shows how to distinguish target-site limits from scraping-API limits, parse `Retry-After` correctly, coordinate cooldowns across workers, and prevent a retry storm. The goal is sustained collection of authorized data with predictable load—not the largest possible burst.

On this page

1. HTTP 429 identifies a rate limit, not its cause2. Find the layer that returned the 4293. Treat Retry-After as the minimum cooldown4. Combine bounded concurrency with a shared cooldown5. Enforce one budget across every worker6. Recover throughput gradually after a 4297. HTTP 429 troubleshooting matrix8. Measure accepted records, not request volume9. Use Web Unlocker without hiding rate-control decisions10. FAQ

1.HTTP 429 identifies a rate limit, not its cause

RFC 6585 defines 429 Too Many Requests but leaves the counting method to the server. A limit may be keyed by IP address, account, API key, cookie, route, or a combination. It may measure a time window or concurrent work, so a new IP does not necessarily reset it.

Do not group 429 with every access failure. HTTP 403 often represents authorization or policy enforcement; 503 indicates temporary service unavailability; a connection timeout means no response completed. These conditions need different controls. Log the status, response body category, relevant headers, target hostname, credential or tenant identifier, and current in-flight count before selecting a remedy.

  • 429 from the target: reduce traffic to that target and honor its published policy.
  • 429 from a scraping API: reduce account-wide request pressure or concurrent jobs.
  • 403 or login redirect: investigate authorization, access policy, or expired state; do not retry as a rate limit.
  • 503 with Retry-After: pause for service recovery, but track it separately from client rate limiting.

2.Find the layer that returned the 429

A managed scraper has at least two HTTP conversations: your client calls the scraping service, and the service calls the target. With OmniScrape, an outer HTTP 429 is an API-layer signal. When the API returns a JSON response, inspect `data.status_code` for the target's HTTP status as documented in the scrape API reference. This distinction tells you whether to slow all OmniScrape workers or only jobs for one target domain.

Use the current OmniScrape rate-limit guide and error reference for platform behavior. Keep account capacity in configuration instead of copying a possibly stale plan limit into code.

  • All domains receive outer 429s: inspect the shared API account budget and total in-flight work.
  • One domain returns target status 429: apply a domain-specific cooldown and lower its request rate.
  • One credential or tenant fails: isolate its queue; do not pause unrelated tenants automatically.
  • 429s begin only after scaling workers: the limit is probably shared, while each worker assumes it owns the full budget.

3.Treat Retry-After as the minimum cooldown

The `Retry-After` field can contain either delay-seconds or an HTTP date. Both forms are defined by RFC 9110. Parse both, clamp past dates to zero, and account for clock skew when a server sends a date. If the field is absent or malformed, fall back to capped exponential backoff with random jitter.

A fixed sleep in each worker is not enough. If 100 workers receive 429 together and sleep for the same duration, they wake together and create another burst. Publish the longest active cooldown to shared state and add jitter to unscheduled retries. `Retry-After` is a lower bound: local policy may wait longer when recent attempts are still failing.

parse both valid Retry-After forms
python
12345678910111213141516171819from datetime import datetime, timezone
from email.utils import parsedate_to_datetime


def retry_after_seconds(value: str | None) -> float | None:
    if not value:
        return None

    value = value.strip()
    if value.isdigit():
        return float(value)

    try:
        retry_at = parsedate_to_datetime(value)
        if retry_at.tzinfo is None:
            retry_at = retry_at.replace(tzinfo=timezone.utc)
        return max(0.0, (retry_at - datetime.now(timezone.utc)).total_seconds())
    except (TypeError, ValueError, OverflowError):
        return None

4.Combine bounded concurrency with a shared cooldown

A semaphore limits simultaneous requests, but it is not a rate limiter. Ten short requests can pass through one slot much faster than ten long requests. Use the semaphore to protect connection and job capacity, then add a time-based budget when the server publishes a request rate. Python's official `asyncio.Semaphore` documentation describes the in-process concurrency primitive.

The example checks both status layers and shares a cooldown in one process. In a multi-process deployment, move `cooldown_until` to Redis or another atomic store. Its configuration values are examples; derive yours from current policy and measured workload.

coordinate 429 cooldowns across concurrent tasks
python
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980import asyncio
import os
import random
import time

import httpx

API_URL = "https://api.omniscrape.io/v1/scrape"


class RateAwareScraper:
    def __init__(self, max_in_flight: int = 4) -> None:
        self.semaphore = asyncio.Semaphore(max_in_flight)
        self.cooldown_until = 0.0
        self.cooldown_lock = asyncio.Lock()
        self.client = httpx.AsyncClient(
            headers={"X-API-Key": os.environ["OMNISCRAPE_KEY"]},
            timeout=90,
            limits=httpx.Limits(max_connections=max_in_flight),
        )

    async def wait_for_cooldown(self) -> None:
        delay = self.cooldown_until - time.monotonic()
        if delay > 0:
            await asyncio.sleep(delay)

    async def extend_cooldown(self, delay: float) -> None:
        async with self.cooldown_lock:
            self.cooldown_until = max(
                self.cooldown_until,
                time.monotonic() + delay,
            )

    async def scrape(self, url: str) -> dict:
        for attempt in range(5):
            await self.wait_for_cooldown()

            async with self.semaphore:
                # Recheck after waiting for a slot; another task may have
                # extended the cooldown while this task was queued.
                await self.wait_for_cooldown()
                response = await self.client.post(
                    API_URL,
                    json={"url": url, "mode": "auto", "output_format": "html"},
                )

            try:
                body = response.json() if response.content else {}
            except ValueError:
                body = {}
            target_status = body.get("data", {}).get("status_code")
            rate_limited = response.status_code == 429 or target_status == 429

            if not rate_limited:
                response.raise_for_status()
                if not body.get("success"):
                    raise RuntimeError(body.get("error", "scrape failed"))
                return body

            header_delay = retry_after_seconds(response.headers.get("Retry-After"))
            jitter_delay = random.uniform(0, min(60, 2 ** attempt))
            delay = max(header_delay or 0, jitter_delay)
            await self.extend_cooldown(delay)

        raise RuntimeError("Rate-limit retry budget exhausted")

    async def close(self) -> None:
        await self.client.aclose()


async def main() -> None:
    scraper = RateAwareScraper(max_in_flight=4)
    try:
        result = await scraper.scrape("https://example.com/")
        print(result.get("success"))
    finally:
        await scraper.close()


asyncio.run(main())

5.Enforce one budget across every worker

A per-process limiter multiplies capacity by the process count. Put the authoritative token bucket, next-allowed timestamp, or leased concurrency counter in shared storage. Key target budgets by normalized hostname and account budgets by credential or tenant.

Backpressure must also reach the queue. During a cooldown, delay eligible jobs instead of letting workers repeatedly dequeue and requeue them. Preserve the original attempt count and deadline, and use idempotent writes because a response can complete just before a client disconnects. The web scraping data pipeline guide covers queue leases, retries, and dead-letter handling in more depth.

  • Global limiter: protects the scraping account and shared infrastructure.
  • Per-domain limiter: prevents one source from consuming the entire global budget.
  • Per-tenant limiter: contains one customer's burst without blocking every customer.
  • Delayed queue: makes cooldowns schedulable without occupying worker connections.

6.Recover throughput gradually after a 429

After the cooldown, do not release the whole backlog. Send a small probe, validate the response, then increase traffic in measured steps. A new 429 should reduce the affected budget again. This adapts to policy changes without embedding a guessed permanent limit.

Keep retry traffic inside the same budget as first attempts. Reserving unlimited capacity for retries creates a second request stream exactly when the service is overloaded. Cap attempts and total elapsed time; when the budget is exhausted, delay or dead-letter the job instead of looping forever. The web scraping timeout guide explains how retry time fits inside a job deadline.

7.HTTP 429 troubleshooting matrix

Use the smallest corrective action that matches the evidence. A rate limit is a control signal, not proof that a target requires a browser or a different proxy.

  • 429 with Retry-After — pause at least that long, publish the cooldown, and resume gradually.
  • 429 without Retry-After — use capped backoff with jitter and lower the shared request budget.
  • 429s across unrelated targets — inspect the API account limit, credential scope, and total in-flight jobs.
  • 429s on one target only — reduce that hostname's rate and check its official API or published crawling policy.
  • Latency rises before 429 — treat queueing delay as an early warning and reduce concurrency before retries accumulate.
  • 429 rate grows after adding workers — replace per-worker limits with one distributed limiter.
  • 403 or challenge page — leave the 429 path; diagnose access policy or protected-page retrieval separately.

8.Measure accepted records, not request volume

Track 429 count by layer, domain, tenant, and credential; parsed Retry-After duration; active cooldowns; queue age; in-flight work; retry attempts; and records that pass validation. Requests started per second is a poor success metric when retries and empty responses inflate the count. The useful outcome is fresh, valid records delivered within the job deadline.

Alert on sustained limits and jobs reaching the attempt cap. Keep headers and a redacted body sample for diagnosis, but never log API keys, authorization headers, session cookies, or personal data. Separate platform and target 429s so the on-call action is clear.

9.Use Web Unlocker without hiding rate-control decisions

OmniScrape Web Unlocker can handle the retrieval layer for independent authorized pages that need managed routing, rendering, or structured extraction. Your application still owns queue scheduling, account-wide concurrency, target-specific cooldowns, validation, and idempotent storage. A managed fetch does not remove the obligation to respect a target's rate limits.

Start with a representative URL set, log both API and target statuses, and tune from evidence. Use Browser-as-a-Service only when the workflow requires stateful browser interaction; a 429 by itself is not a reason to open a browser. Collect only data you are authorized to use and respect applicable law, website terms, robots directives where applicable, privacy obligations, and published limits.

  • Identify the limiting layer before changing proxies, modes, or worker count.
  • Parse both Retry-After formats and share the longest active cooldown.
  • Bound concurrency and request rate separately.
  • Coordinate budgets across processes, tenants, and domains.
  • Resume with probes, validate content, and increase traffic gradually.
  • Cap retry attempts and route exhausted jobs to a delayed or dead-letter queue.

Frequently asked questions

What does HTTP 429 mean when web scraping?

It means the server believes the client exceeded a request limit. The limit may be based on an IP, account, API key, cookie, endpoint, concurrent jobs, or another scope, so identify the returning layer and counter before changing the scraper.

How long should a scraper wait after a 429 response?

Honor Retry-After as the minimum wait when it is present. It may contain seconds or an HTTP date. Without a valid value, use capped exponential backoff with random jitter and lower the shared request budget if 429 responses continue.

Does limiting concurrency prevent HTTP 429 errors?

Not always. A semaphore limits simultaneous requests, but fast requests can still exceed a requests-per-second or requests-per-minute quota. Use separate controls for in-flight concurrency and time-based request rate.

Should I rotate proxies after receiving a 429?

Not automatically. The limit may be tied to an account, API key, cookie, or target-wide policy rather than the IP. Rotating can also break a valid session. First honor the cooldown and determine the limiter's scope.

Can OmniScrape prevent every target-site 429?

No service can guarantee that. OmniScrape can manage page retrieval, while your application should still control queue rate, concurrency, cooldowns, validation, and compliance with the target's published policies.

Related guides

  • Web Scraping Timeouts: Diagnose, Budget, and Retry
  • Web Scraping Data Pipeline: Queue, Validate, and Store
  • HTTPX Web Scraping: Async Python with OmniScrape
  • Web Scraping API: Endpoint, Modes, Output Formats & Integration Patterns
  • Web Scraping Without Getting Blocked
  • Private Proxies for Scraping: What the Term Still Means

Ready to scrape without blocks?

Published unit rates, clear plan limits, and billing tied to successful work. No hidden fees and no guessing what a scrape will cost. Free trial credit on signup. No credit card required.

Ready to get started?

Start scraping protected sites today. No credit card required.

OmniScrapeOmniScrape

Web scraping infrastructure for developers. One API call to bypass any protection.

All systems operational

Payments accepted

Credit / Debit CardVisaMastercardCryptoBTCUSDTETH50+ coins

Product

  • Web Unlocker
  • Browser-as-a-Service
  • Residential Proxies
  • Pricing

Developers

  • API Reference ↗
  • Quickstart ↗
  • All Guides
  • Use Cases
  • Status

Company

  • About
  • Contact

Legal

  • Privacy Policy
  • Terms of Service
  • Refund Policy
  • Cookie Policy
  • Acceptable Use

Popular guides

  • How to Bypass Cloudflare When Web Scraping
  • How to Bypass DataDome When Web Scraping
  • How to Bypass Akamai Bot Manager When Web Scraping
  • How to Bypass F5 BIG-IP Bot Defense When Web Scraping
  • Web Scraping API: Endpoint, Modes, Output Formats & Integration Patterns
  • Web Scraping with Python
  • Scrape JavaScript-Rendered Pages: SPAs, Hydration, and Hidden APIs
  • Web Scraping Without Getting Blocked
  • Headless Browser Scraping: When to Use It and How to Do It Right
  • Rotating Proxies for Web Scraping: Policies, Session Binding, and Geo Pools
  • OmniScrape vs ScrapingBee
  • OmniScrape vs ZenRows
Browse all guides →

OmniScrape is a product of PT Data Digital Grup. Copyright ©2026.

PrivacyTermsRefundsAcceptable Use