HTTP 429 Too Many Requests beim Scraping beheben

http 429 too many requests - How to Fix HTTP 429 Too Many Requests While Scraping

HTTP 429 Too Many Requests heißt langsamer werden, nicht verschwinden. Der Server sagt Ihnen, dass er Ihren Traffic weiter will, nur in geringerem Tempo, und meist nennt er genau die Wartezeit. Die Lösung ist deshalb fast nie ein Proxy oder ein neuer User-Agent. Sie besteht darin, einen Header zu lesen, richtig zu warten und die Zahl gleichzeitiger Anfragen zu begrenzen. Dieser Beitrag behandelt alle drei Punkte und zeigt dann, wie Sie eine echte Ratenbegrenzung von einer Bot-Sperre im gleichen Statuscode unterscheiden, denn beide brauchen die gegenteilige Reaktion.

Was Sie brauchen

  • Python 3.10 oder neuer mit der requests-Bibliothek für die Beispiele. Die Logik lässt sich direkt auf jeden HTTP-Client übertragen.
  • Ein Terminal, damit Sie die Antwort-Header ansehen können, bevor Sie Retry-Code schreiben.
  • CapSkip laufend, und zwar nur für den letzten Abschnitt, entweder im lokalen Modus auf der Loopback-Adresse oder im Servermodus auf einer Maschine, die Ihre Worker erreichen. Beide sind unter Verbindungseinstellungenbeschrieben, entscheiden Sie sich also vorher für einen.

Schritt 1: die Antwort lesen, bevor Sie sie wiederholen

Die meiste 429-Behandlung wird blind geschrieben, und deshalb funktioniert sie nicht. Sehen Sie sich zuerst die echte Antwort an. Mit dem Statuscode kommen Header, die Ihnen sagen, wie hoch das Limit ist und wann es zurückgesetzt wird, und verschiedene Dienste verwenden verschiedene.

# No install needed. Dump headers, throw the body away.
curl -sS -o /dev/null -D - "https://example.com/api/items?page=2"

# Look for these, in this order of usefulness:
#   Retry-After: 30           seconds, or an HTTP date
#   RateLimit-Reset: 1724500000
#   X-RateLimit-Remaining: 0
#   RateLimit-Limit: 100

Der entscheidende ist Retry-After. Er ist genau für diese Situation definiert und kommt in zwei Formen: eine Zahl von Sekunden oder ein absolutes HTTP-Datum. Beide sind erlaubt, beide kommen in der Praxis vor, und Code, der die Zahl voraussetzt, bricht auf den Seiten, die das Datum senden. MDN dokumentiert beide Formen, und die Referenzseite zum Statuscode 429 ist ebenfalls zwei Minuten Ihrer Zeit wert.

Parsen Sie ihn defensiv, halten Sie sich daran, wenn er da ist, und fallen Sie auf Ihren eigenen Zeitplan zurück, wenn nicht:

# pip install requests
from email.utils import parsedate_to_datetime
from datetime import datetime, timezone

def retry_delay(response, fallback):
    """Seconds to wait, from Retry-After if the server sent one."""
    raw = response.headers.get("Retry-After")
    if not raw:
        return fallback
    try:
        return max(0.0, float(raw))          # the delay-seconds form
    except ValueError:
        pass
    try:
        when = parsedate_to_datetime(raw)    # the HTTP-date form
        return max(0.0, (when - datetime.now(timezone.utc)).total_seconds())
    except (TypeError, ValueError):
        return fallback

Begrenzen Sie, was zurückkommt. Ein Server, der 3600 Sekunden verlangt, sagt Ihnen, dass Sie eine Stunde aufhören sollen, und ein Worker, der so lange brav in einem Request-Handler schläft, sieht für alles darüber wie ein Hänger aus. Nehmen Sie den kleineren Wert aus Header und eigener Obergrenze und entscheiden Sie dann getrennt, ob Sie den Job vertagen.

Schritt 2: exponentiell zurückweichen, mit Jitter

Wenn es kein Retry-After gibt, dem Sie folgen könnten, verdoppeln Sie die Wartezeit bei jedem Versuch und fügen Zufall hinzu. Das Verdoppeln verhindert, dass Sie einen bereits kämpfenden Dienst weiter hämmern. Der Zufall verhindert, dass zwanzig Ihrer eigenen Worker, die alle in derselben Sekunde ans Limit stoßen, für immer in derselben Sekunde neu versuchen.

# pip install requests
import random, time, requests

def get_with_backoff(url, attempts=5, base=1.0, ceiling=60.0):
    for attempt in range(attempts):
        response = requests.get(url, timeout=30)
        if response.status_code != 429:
            return response
        # Full jitter: sleep somewhere in [0, base * 2 ** attempt].
        window = min(ceiling, base * (2 ** attempt))
        delay = retry_delay(response, random.uniform(0, window))
        time.sleep(min(delay, ceiling))
    raise RuntimeError(f"still rate limited after {attempts} attempts")

# Waits land near 0-1s, 0-2s, 0-4s, 0-8s, 0-16s unless the
# server named a delay, in which case that wins.

Voller Jitter, also ein Zufallswert zwischen null und dem Fenster statt Fenster plus kleinem Wackeln, entkoppelt eine Flotte am schnellsten. Wenn Sie das nicht selbst schreiben wollen: urllib3 hat es eingebaut, braucht aber zwei explizit gesetzte Argumente, um nützlich zu sein:

# pip install requests
import requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry

retry = Retry(
    total=5,
    # status_forcelist defaults to none, so 429 is NOT retried
    # unless you list it here yourself. This is the usual bug.
    status_forcelist=[429, 500, 502, 503, 504],
    backoff_factor=1,        # 1 * 2 ** previous_retries seconds
    backoff_jitter=1.0,      # urllib3 2.x only
    allowed_methods=["GET", "HEAD"],
)

session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=retry))

Zwei Details lohnen sich zu dieser Klasse. Retry-After beachtet sie bereits selbst, weil 429 einer der drei Statuscodes in ihrem Set RETRY_AFTER_STATUS_CODES neben 413 und 503 ist und respect_retry_after_header standardmäßig true lautet. Aber status_forcelist ist standardmäßig leer, deshalb wiederholt ein frisches Retry-Objekt einen 429 erst, wenn Sie ihn dort nennen. Man nimmt üblicherweise das Gegenteil an und wundert sich dann, warum der Adapter nichts getan hat.

Schritt 3: Parallelität begrenzen statt härter zu wiederholen

Retry-Logik behandelt das Symptom. Wenn 429 gleichmäßig statt in Schüben kommen, verlangen Sie einfach mehr als erlaubt, und die Lösung heißt weniger senden. Ein Semaphore plus eine Untergrenze für den Abstand zwischen Anfragen behebt mehr Ratenbegrenzung als jede Backoff-Kurve.

# Standard library only.
import asyncio

# Six in flight is a sane starting point for an unknown API.
gate = asyncio.Semaphore(6)
MIN_GAP = 0.2          # seconds between starts, per worker

async def fetch(client, url):
    async with gate:
        response = await client.get(url)
        await asyncio.sleep(MIN_GAP)
        return response

# Tune down on the first 429, and stay there for a while.
# Tuning back up too eagerly just rediscovers the limit.

Der erste Impuls nach einem 429 ist, dieselbe Last auf mehr IPs zu verteilen. Bei manchen Zielen wirkt das, doch es ist eine eigene Entscheidung mit eigenen Abwägungen, die wir separat im Leitfaden zur CAPTCHA-Proxy-Rotationbehandelt haben. Tun Sie es als Kapazitätsentscheidung und nicht als Weg, das Lesen eines Headers zu umgehen.

Ein 429 ist kein 403, und eine Challenge ist beides nicht

Hier wird 429-Behandlung am teuersten falsch. Ratenbegrenzung und Bot-Erkennung sind verschiedene Systeme, die sich manchmal einen Statuscode teilen, und sie wollen Gegenteiliges von Ihnen. Ein HTTP 429 Too Many Requests von einem Rate Limiter ist eine Terminanweisung, derselbe Code von einer Anti-Bot-Edge ist eine Absage. Höflich zurückzuweichen bringt gegen eine Bot-Sperre eine verlorene Stunde. Hart zu wiederholen bringt bei einer echten Ratenbegrenzung eine IP-Sperre.

Was Sie bekommen habenWas es meist bedeutetWas wirklich hilft
429 mit einem Retry-After-HeaderEine echte, dokumentierte RatenbegrenzungGenau so lange warten und dann das Tempo senken
429 ohne Header und mit HTML-BodyEine Edge- oder Anti-Bot-Schicht, nicht die APIAls Sperre behandeln und nicht als Limit
403 kommt sofortFingerprint, TLS oder IP-ReputationDen Client korrigieren, denn Warten ändert nichts
503 mit Retry-AfterÜberlastet oder in WartungDerselbe Backoff-Pfad wie bei 429
200 mit einer Challenge-SeiteSie wurden bewertet und unterbrochenDie Challenge lösen und weitermachen

Die letzte Zeile überrascht viele, weil formal nichts fehlgeschlagen ist. Die Anfrage kam mit 200 zurück und der Body ist eine Challenge-Seite statt Ihrer Daten, deshalb wird eine Retry-Schleife, die nur den Statuscode ansieht, munter für immer dagegen hämmern. Prüfen Sie auf den erwarteten Marker im Body und nicht nur auf die Statuszeile. Wenn Sie eine Challenge gefunden haben, ist Zurückweichen nicht die Antwort, Lösen schon.

Halten Sie Ihre Solve-Schleife von derselben Falle fern

Die Polling-Schleife, die auf eine CAPTCHA-Antwort wartet, ist selbst eine Retry-Schleife, und selbstgebaute machen genau die Fehler von oben. Zwei Dinge machen das bei CapSkip einfacher als bei einem abgerechneten Dienst. Es läuft auf Ihrer eigenen Hardware, es gibt also kein Kontingent pro Lösung, das sich erschöpfen lässt, und keine eigene Ratenbegrenzung, in die man laufen kann. Und die SDKs weichen schon für Sie zurück: das Polling beginnt bei einer Viertelsekunde und wächst bis pollingInterval, das eine Obergrenze ist und kein fester Abstand.

# pip install capskip
from capskip import CapSkip, NetworkException, TimeoutException

# Local mode. In Server mode, host is the solver box's address.
solver = CapSkip(host="127.0.0.1", port=8080, pollingInterval=2)

try:
    result = solver.recaptcha(
        sitekey="YOUR_SITEKEY",
        url="https://example.com/page-with-recaptcha",
    )
    print(result["code"][:24])   # token, submit it with the form
except TimeoutException:
    # Polling ran past recaptchaTimeout, 300 seconds by default.
    print("gave up waiting, try again or lower the timeout")
except NetworkException:
    # The solver is not reachable on that host and port.
    print("check CapSkip is running and the mode you set")

pollingInterval zu senken lässt eine Antwort früher eintreffen und kostet nichts, und diese Wahl haben Sie bei einer abgerechneten API im Grunde nicht. Wenn Sie statt eines SDK die rohen HTTP-Endpunkte abfragen, stehen die empfohlenen Wartezeiten je CAPTCHA-Typ in der API-Dokumentation, zusammen mit der Antwort, die Sie erhalten, solange eine Lösung noch läuft.

Den Löser stattdessen auf einem Server betreiben

Ratenbegrenzung ist meist ein Problem einer Flotte und nicht eines einzelnen Skripts, und eine Flotte teilt keine Loopback-Adresse. Die Verbindungseinstellungen decken beide Fälle ab:

ModusLauscht aufSinnvoll, wenn
Lokal127.0.0.1, nur dieses GerätIhr Scraper und der Löser laufen auf einer Maschine
ServerIhre Netzwerkadresse oder öffentliche IPWorker, Container, ein VPS oder eine gehostete Plattform rufen über die API an

Richten Sie den SDK-Host auf die Maschine mit dem Löser, und in Ihrem Code ändert sich sonst nichts, sodass zehn Worker eine Instanz nutzen können. Eine statische öffentliche IP ist empfehlenswert, wenn die Aufrufer außerhalb Ihres eigenen Netzes sitzen. Die Details stehen unter Verbindungseinstellungen, und der Servermodus bleibt Ihre Hardware und bleibt ohne Zähler: er verschiebt, wo der Löser läuft, nicht wem er gehört.

FAQ

Sollte ich Retry-After immer befolgen?

Befolgen Sie ihn, aber mit Obergrenze. Er ist das zuverlässigste Signal dafür, wann sich das Fenster wieder öffnet, ihn zu ignorieren heißt also schlechter zu raten, als der Server es Ihnen schon gesagt hat. Ein Wert von einer Stunde ist allerdings eine andere Entscheidung: parken Sie den Job und kommen Sie zurück, statt einen Worker schlafend zu halten, denn alles darüber liest das als Hänger.

Wie unterscheide ich eine Ratenbegrenzung von einer Bot-Sperre?

Sehen Sie sich an, was mit dem Statuscode kam. Ein echtes Limit ist maschinenlesbar: ein Retry-After- oder RateLimit-Header, ein kleiner JSON-Body und gleichmäßiges Verhalten, wenn Sie warten. Eine Bot-Sperre schickt eine HTML-Seite, keine Zeitangabe und oft dieselbe Antwort, egal wie lange Sie sie liegen lassen. Die Zweite will einen anderen Client und keinen längeren Schlaf.

Gibt der Löser jemals einen 429 zurück?

Nein. CapSkip läuft auf Hardware, die Sie kontrollieren, ohne Kontingent pro Lösung, es gibt also kein Abrechnungsfenster, das sich erschöpfen lässt, und kein vorgelagertes Limit, in das man laufen kann. Scheitert ein Solve-Aufruf, bekommen Sie einen Netzwerkfehler, weil der Daemon nicht erreichbar ist, oder einen Timeout, weil das Polling seine Obergrenze überschritten hat. Beides bedeutet etwas Lokales, prüfen Sie also Host, Port und den eingestellten Modus.

Meine Worker laufen auf einer gehosteten Plattform. Wohin kommt der Löser?

Auf eine Maschine von Ihnen, die die Plattform erreichen kann, mit dem Löser im Servermodus. Gehostete Runner und verwaltete Automatisierungsplattformen sehen Ihre Loopback-Adresse nicht, binden Sie die API also an Ihre Netzwerk- oder öffentliche IP und richten Sie jeden Worker darauf. Eine Instanz bedient die ganze Flotte, und ein Tunnel ist nicht nötig.

Die kürzeste Fassung

HTTP 429 Too Many Requests ist ein Terminproblem, behandeln Sie es also als solches. Lesen Sie Retry-After und befolgen Sie ihn bis zu einer Obergrenze, die Sie wählen. Fallen Sie auf exponentielles Zurückweichen mit vollem Jitter zurück, wenn der Header fehlt. Senken Sie dann Ihre Parallelität, denn gleichmäßige 429 sind ein Kapazitätsproblem, das keine Retry-Kurve löst. Und prüfen Sie den Body, bevor Sie überhaupt wiederholen, denn eine Challenge-Seite kommt mit einem völlig gesunden Statuscode und will gelöst und nicht abgewartet werden. Dafür passt ein unbegrenzter Captcha-Löser , der lokal läuft, und wie er in einen Worker-Pool passt, zeigen die Hinweise zum Captcha-Löser für Web Scraping im Detail.