Fehlerbehebung

Fehler 429 Too Many Requests: Bedeutung, nginx-Limit anpassen und bei fremden APIs richtig warten

Fehler 429 Too Many Requests heißt auf Deutsch „zu viele Anfragen“: Ein Rate Limit hat gegriffen. So findest du das Limit und behebst den Fehler.

Veröffentlicht ~10 Min. Lesezeit
Auf dieser Seite

Kurz gesagt: Der Fehler 429 Too Many Requests heißt auf Deutsch „zu viele Anfragen“: Ein Rate Limit hat gegriffen. Der Server hat deine Anfragen in einem Zeitfenster gezählt und lehnt den Rest ab, bis das Fenster abgelaufen ist. Das ist also Absicht und kein Absturz. Betreibst du den Server selbst, finde das Limit, das gegriffen hat (nginx limit_req, deine Anwendung oder Cloudflare), und stelle es so ein, dass echte Nutzer durchkommen. Bekommt dein Skript oder Bot die 429 von einer fremden API, bremse ab und warte so lange, wie die Antwort es verlangt, meist in einem Retry-After-Header.

zuerst: wessen Limit ist es?
curl -s -D - -o /dev/null https://example.com/api/items
# achte auf diese Response-Header:
#   server:          nginx, cloudflare oder der Name der API
#   retry-after:     Sekunden (oder ein Datum) bis zum nächsten Versuch
#   x-ratelimit-*:   die eigenen Zähler der API (Discord, GitHub und viele andere)

Was bedeutet Fehler 429 Too Many Requests?

RFC 6585 definiert den Code. Die Antwort soll den Zustand erklären und kann einen Retry-After-Header mitschicken, aber nichts zwingt den Server dazu, und Caches dürfen eine 429 nicht speichern. Wie gezählt wird, überlässt die Spezifikation dem Server: pro IP-Adresse, pro API-Key, pro Konto, pro Route oder nach mehreren davon gleichzeitig. Ein Client kann also unter einem Limit liegen und zur selben Zeit ein anderes überschreiten.

Zwei Varianten sorgen für Verwirrung. Der eigene Limiter von nginx antwortet standardmäßig mit 503, nicht mit 429, solange du nicht limit_req_status 429 setzt; ein „503 Service Unavailable“ unter Last kann also dasselbe Limit sein. Und manche APIs, darunter GitHub, beantworten ein Rate Limit mit 403 oder 429 und sagen es im Header x-ratelimit-remaining: 0.

QuelleWoran du sie erkennstWo du nachsiehst
nginx limit_req / limit_connKleine HTML-Seite, Server: nginx (oder eine 503, wenn limit_req_status nicht gesetzt ist)limiting requests oder limiting connections im Error-Log von nginx
Der eigene Limiter deiner AnwendungJSON oder eine Seite im Stil der App; das Error-Log von nginx bleibt stillDas Log der Anwendung
Rate-Limiting-Regel bei CloudflareCloudflare-Seite: Error 1015, You are being rate limitedDie Sicherheitsereignisse im Cloudflare-Dashboard
Eine fremde APIx-ratelimit-*-Header, ein Feld retry_after im JSON-BodyDas Log deines Skripts oder Bots

Ist es dein nginx? Prüfe das Error-Log

nginx protokolliert standardmäßig jede abgelehnte Anfrage auf der Stufe error und nennt dabei die Zone, die sie abgelehnt hat:

Debian · Ubuntu
sudo grep 'limiting requests' /var/log/nginx/error.log | tail -n 5

# eine typische Zeile:
# [error] 2216#2216: *33 limiting requests, excess: 4.964 by zone "api",
#   client: 203.0.113.50, server: example.com, request: "GET /api/items HTTP/1.1"

Der Zonenname verrät, welche limit_req_zone gegriffen hat; excess gibt an, wie weit der Client über der eingestellten Rate lag, als nginx ihn abwies. Dann sieh nach, wer die 429 bekommt und bei welchen URLs. Im Standard-Logformat combined steht der Status im neunten Feld:

wer begrenzt wird
sudo awk '$9 == 429 {print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
sudo awk '$9 == 429 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head

Sind die obersten „Clients“ eine Handvoll Adressen von Cloudflare oder einem Load Balancer, spring zu Schritt 2 unten. Sind es dein eigenes Monitoring, die Webhooks eines Zahlungsanbieters oder dein Büro, hilft Schritt 3.

Wie behebe ich 429-Fehler durch nginx limit_req?

Die Ursachen, die häufigste zuerst:

  1. 1Einen Burst ergänzen: Ein Limit ohne Burst lehnt normale Seitenaufrufe ab

    nginx erfasst Anfragen auf die Millisekunde genau, rate=10r/s heißt also in Wahrheit eine Anfrage alle 100 ms. Ein Browser, der eine Seite lädt, schickt ein Dutzend Anfragen auf einmal, und ohne burst wird jede nach der ersten in diesem 100-ms-Fenster abgelehnt. Drei gleichzeitige Anfragen an eine Zone mit 10r/s ohne Burst bekommen deshalb 200, 429, 429, sobald limit_req_status 429 wie unten gesetzt ist.

    /etc/nginx/conf.d/ratelimit.conf (http-Kontext)
    limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
    limit_req_status 429;      # Standard ist 503
    limit_conn_status 429;     # dasselbe für limit_conn, falls du es nutzt
    in deinem server-Block
    location /api/ {
        limit_req zone=perip burst=20 nodelay;
        proxy_pass http://127.0.0.1:3000;
    }

    burst=20 erlaubt einem Client 20 Anfragen über der Rate, bevor nginx ablehnt, und die Plätze füllen sich mit der eingestellten Rate wieder auf. Mit nodelay werden diese zusätzlichen Anfragen sofort beantwortet; ohne stellt nginx sie in eine Warteschlange und gibt sie im Takt der Rate frei, Besucher sehen dann langsame Antworten statt Fehlern. delay=8 mischt beides: Die ersten 8 zusätzlichen Anfragen gehen sofort durch, der Rest wird verteilt. Eine Zone mit 10 MB reicht für die meisten Websites: Auf 64-Bit-Systemen fasst ein Megabyte etwa 8.000 Client-Zustände.

  2. 2Dafür sorgen, dass nginx die echte Client-IP sieht

    Hinter Cloudflare, einem Load Balancer oder einem anderen Proxy ist $binary_remote_addr die Adresse des Proxys. Alle Besucher teilen sich dann wenige Buckets und laufen gemeinsam ins Limit. Stelle die IP des Besuchers mit dem realip-Modul wieder her:

    /etc/nginx/conf.d/realip.conf
    set_real_ip_from 173.245.48.0/20;
    # ... eine Zeile für jeden Bereich auf https://www.cloudflare.com/ips/
    real_ip_header CF-Connecting-IP;

    Trage nur Adressen ein, über die du wirklich proxyst, sonst kann jeder den Header fälschen und dem Limit ausweichen. Die vollständige Cloudflare-Einrichtung steht in der Anleitung zum nginx-Reverse-Proxy. Büros und Mobilfunknetze hinter Carrier-Grade-NAT teilen sich ebenfalls ganz legitim eine Adresse; halte Limits pro IP auf öffentlichen Seiten deshalb großzügig und streng nur bei Logins.

  3. 3Nur begrenzen, was es braucht, und eigene Tools ausnehmen

    Ein limit_req auf Server-Ebene erfasst auch Bilder, CSS, Health Checks, Uptime-Monitore und Webhooks. Setze strenge Limits auf Login-, Such- und API-Routen und lass statische Dateien durch. Um bekannte Adressen auszunehmen, nutze, dass nginx Anfragen mit leerem Schlüssel nicht zählt:

    /etc/nginx/conf.d/ratelimit.conf (http-Kontext)
    geo $rate_exempt {
        default        0;
        198.51.100.10  1;    # dein Monitoring
        10.0.0.0/8     1;    # internes Netz
    }
    map $rate_exempt $rate_key {
        0  $binary_remote_addr;
        1  "";               # leerer Schlüssel: wird nicht gezählt
    }
    limit_req_zone $rate_key zone=perip:10m rate=10r/s;

    Eine Vererbungsregel erwischt viele: Eine location erbt limit_req vom server-Block nur, wenn sie kein eigenes limit_req hat. Fügst du einer location eines hinzu, gelten die Limits der Server-Ebene dort nicht mehr.

  4. 4Erst messen, dann durchsetzen

    Der Dry-Run-Modus zählt und protokolliert, ohne etwas abzulehnen, und die Variable $limit_req_status zeigt, was passiert wäre. Beides kam 2019 mit nginx 1.17, jedes aktuelle Debian- und Ubuntu-Paket hat es also:

    einen Tag Dry Run, dann zählen
    # http-Kontext
    log_format ratelog '$remote_addr [$time_local] "$request" $status $limit_req_status';
    
    # in der location, die du testest
    limit_req zone=perip burst=20 nodelay;
    limit_req_dry_run on;
    # ein access_log hier ersetzt das geerbte, also auch das normale Log angeben
    access_log /var/log/nginx/access.log;
    access_log /var/log/nginx/ratelimit.log ratelog;
    
    # später:
    grep -c REJECTED_DRY_RUN /var/log/nginx/ratelimit.log
  5. 5Clients sagen, wie lange sie warten sollen

    nginx schickt von sich aus kein Retry-After. Leite die 429 an eine benannte location um, die einen ergänzt, damit sich sauber programmierte Clients an die richtige Wartezeit halten, statt den Server weiter zu bombardieren:

    in deinem server-Block
    location /api/ {
        limit_req zone=perip burst=20 nodelay;
        error_page 429 @ratelimited;
        proxy_pass http://127.0.0.1:3000;
    }
    
    location @ratelimited {
        add_header Retry-After 10 always;
        default_type application/json;
        return 429 '{"error":"rate_limited","retry_after":10}';
    }
    anwenden
    sudo nginx -t && sudo systemctl reload nginx

Und wenn das Error-Log von nginx still bleibt?

Dann kommt die 429 von woanders. Ist der Body das JSON deiner Anwendung, hat ihr eigener Limiter gegriffen, und dasselbe IP-Problem aus Schritt 2 gilt eine Ebene tiefer. Express zum Beispiel sieht jede Anfrage als von nginx kommend, bis du app.set('trust proxy', 1) setzt, und dafür muss nginx X-Forwarded-For mitschicken.

Zeigt die Seite Error 1015 von Cloudflare, hat eine Rate-Limiting-Regel im Cloudflare-Dashboard die Anfrage blockiert, bevor sie deinen Server überhaupt erreicht hat; deshalb zeigen deine Logs nichts. Diese Regeln antworten standardmäßig mit 429, und du kannst jeden Code von 400 bis 499 einstellen. Lösen echte Besucher eine davon aus, erhöhe ihre Anfragezahl oder verlängere ihren Zählzeitraum, statt sie zu löschen. Die Troubleshooting-Seite von Cloudflare zu 1015 warnt selbst, dass Fenster von einer Sekunde zu streng sind, und empfiehlt 10 Sekunden oder mehr.

429 von einer fremden API? Richtig abbremsen

  • Sofort aufhören. Anfragen, die du während der Sperre schickst, werden ohnehin abgelehnt, und manche APIs rechnen sie dir an. Discord zählt die meisten 429 als ungültige Anfragen.
  • So lange warten, wie der Server sagt. Retry-After enthält entweder eine Anzahl Sekunden oder ein HTTP-Datum. Viele APIs geben einen eigenen Hinweis dazu: Discord liefert retry_after im JSON-Body, Telegram parameters.retry_after, GitHub den Zeitstempel x-ratelimit-reset.
  • Kein Hinweis? Exponentiell länger warten, mit Jitter. Warte eine zufällige Zeit zwischen null und base × 2^attempt, nach oben begrenzt. Der Zufall verhindert, dass viele Clients im Gleichschritt erneut anfragen und gemeinsam wieder ins Limit laufen.
  • Die Wiederholungen begrenzen und dann mit einer deutlichen Fehlermeldung abbrechen, damit ein hängender Job nicht ewig in der Schleife läuft.
curl
# wiederholt bei Timeouts und 408, 429, 500, 502, 503, 504 (neueres curl auch 522, 524)
# bis zu 5-mal, verdoppelt die Wartezeit ab 1 s und beachtet Retry-After (ab curl 7.66.0)
curl --retry 5 --retry-max-time 120 -sS https://api.example.com/items
Python · requests
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

retry = Retry(
    total=5,
    status_forcelist=[429, 503],
    backoff_factor=1,                 # 0 s, 2 s, 4 s, 8 s ... wenn kein Retry-After kommt
    respect_retry_after_header=True,  # der Standard, nur zur Verdeutlichung
)
session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=retry))

resp = session.get("https://api.example.com/items", timeout=30)

urllib3 wiederholt POST standardmäßig nicht, und das ist richtig: Ein wiederholter POST kann seine Wirkung wiederholen, wenn die API nicht dafür gebaut ist. Sind die Versuche aufgebraucht, wirft requests einen RetryError. Selbst geschrieben ist die Schleife ebenfalls kurz:

Python · Full Jitter von Hand
import random, time
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
import requests

def retry_after_seconds(resp):
    value = resp.headers.get("Retry-After")
    if value is None:
        return None
    if value.strip().isdigit():                  # "120"
        return int(value)
    try:                                         # "Wed, 21 Oct 2026 07:28:00 GMT"
        when = parsedate_to_datetime(value)
    except (TypeError, ValueError):
        return None
    return max(0.0, (when - datetime.now(timezone.utc)).total_seconds())

def get_with_backoff(url, max_tries=6, base=1.0, cap=60.0):
    for attempt in range(max_tries):
        resp = requests.get(url, timeout=30)
        if resp.status_code not in (429, 503) or attempt == max_tries - 1:
            return resp
        wait = retry_after_seconds(resp)
        if wait is None:
            wait = random.uniform(0, min(cap, base * 2 ** attempt))
        time.sleep(wait)

Warum bekommt mein Discord- oder Telegram-Bot 429?

Discord begrenzt jede Route einzeln, in Buckets, dazu kommt eine globale Obergrenze von 50 Anfragen pro Sekunde pro Bot. Antworten enthalten X-RateLimit-Limit, -Remaining, -Reset, -Reset-After und -Bucket; eine 429 ergänzt Retry-After, X-RateLimit-Scope und einen JSON-Body mit retry_after in Sekunden (mit Nachkommastellen) und einem Flag global. discord.py und discord.js stellen Anfragen pro Bucket in eine Warteschlange und warten eine 429 von selbst ab; discord.py protokolliert dabei die Warnung We are being rate limited. Ab und zu ist das in Ordnung. Taucht sie ständig auf, kämpft dein Code gegen das Limit:

  • Eine Schleife, die dieselbe Nachricht bearbeitet oder Rollen oder Nicknames mehrmals pro Sekunde ändert.
  • Massen-DMs oder Massenänderungen an Kanälen ohne Pausen dazwischen.
  • Mehrere Prozesse mit demselben Token. Jede Bibliotheksinstanz verfolgt nur ihre eigenen Anfragen, zusammen überrennen sie die Buckets.
  • Eine Crash-Schleife, die bei jedem Start Slash-Commands registriert. Discord erlaubt 200 neu angelegte Commands pro Tag und Server (Guild).

Telegram liefert die Wartezeit im Body:

Telegram Bot API · 429-Antwort
{"ok": false, "error_code": 429,
 "description": "Too Many Requests: retry after 35",
 "parameters": {"retry_after": 35}}

Die Bot-FAQ von Telegram bittet darum, in einem einzelnen Chat bei etwa einer Nachricht pro Sekunde zu bleiben, in einer Gruppe bei höchstens 20 Nachrichten pro Minute und beim Versand an alle Chats bei rund 30 Nachrichten pro Sekunde; große Aussendungen verteilst du also über Stunden. python-telegram-bot bringt dafür einen optionalen Limiter mit: Installiere python-telegram-bot[rate-limiter] und übergib AIORateLimiter() an den Application-Builder.

Auch der Ort, an dem der Bot läuft, spielt eine Rolle. Discord verhängt die Sperre für ungültige Anfragen pro IP; bei einem kostenlosen Hoster, bei dem sich viele Bots eine Adresse teilen, zählen die Fehler anderer Leute also gegen dich. Ein kleiner VPS mit eigener IPv4 hält diesen Zähler bei dir. Die Tarife findest du auf der Seite Discord-Bot-Hosting, die Einrichtung mit systemd in der Anleitung zum Bot-Hosting (engl.).

Wie verhindere ich, dass 429-Fehler wiederkommen?

  • Auf deinem Server: limit_req_status 429 setzen, jedem Limit einen Burst geben, die echte Client-IP als Schlüssel nehmen, Logins und APIs statt Assets begrenzen, vor dem Durchsetzen einen Dry Run fahren und Retry-After senden.
  • Clients, die immer wieder ins Limit laufen, an die Firewall übergeben: Aktiviere das mitgelieferte Jail nginx-limit-req von fail2ban, das dieselben Zeilen im Error-Log liest.
  • In deinen eigenen Clients: ein gemeinsamer Client pro Token, der die Rate kennt, Retry-After beachten, Backoff mit Jitter, eine Obergrenze für Wiederholungen und ein Cache für Daten, die du immer wieder abrufst.
  • Netzwerk- und Anwendungsschutz getrennt halten. Ständig aktiver DDoS-Schutz filtert Floods, bevor sie den Server erreichen, kennt aber deine Limits pro Nutzer nicht. Was DDoS-Schutz abdeckt (engl.) erklärt, wo das eine endet und das andere beginnt.

Alles oben läuft auf jedem KVM-VPS mit Root-Zugriff und dem Standardpaket von nginx unter Ubuntu oder Debian.

Häufige Fragen

Was bedeutet Fehler 429 Too Many Requests auf Deutsch?

Too Many Requests heißt „zu viele Anfragen“. Ein Rate Limit hat in kurzer Zeit zu viele Anfragen von deiner IP-Adresse, deinem Konto oder deinem API-Key gezählt und lehnt den Rest eine Weile ab. Kaputt ist meist nichts: Warte eine Minute und versuche es erneut. Passiert es immer wieder, schickt etwas auf deiner Seite Anfragen in einer Schleife, etwa ein Skript, eine Browser-Erweiterung oder mehrere Tabs, die dieselbe Seite abfragen.

Wie lange sollte ich nach einer 429-Antwort warten?

So lange, wie der Header Retry-After angibt, entweder eine Anzahl Sekunden oder ein Datum. Discord und Telegram schreiben die Wartezeit zusätzlich als retry_after in den JSON-Body. Gibt der Server keinen Hinweis, warte exponentiell länger mit zufälligem Jitter, beginnend bei etwa einer Sekunde, und hör nach einer festen Zahl von Versuchen auf.

Warum liefert das Rate Limiting von nginx 503 statt 429?

limit_req und limit_conn von nginx antworten standardmäßig mit 503 Service Unavailable. Ergänze limit_req_status 429; und limit_conn_status 429; im http-, server- oder location-Block und führe dann sudo nginx -t && sudo systemctl reload nginx aus. So können Clients und Monitoring ein Rate Limit von einem echten Ausfall unterscheiden.