429 bei halbvollem Kontingent: wie max_tokens das Azure-OpenAI-Limit frisst
Azure OpenAI drosselt mit 429, obwohl kaum Tokens verbraucht werden? Die Ratenbegrenzung schätzt mit max_tokens statt mit der echten Antwortlänge. Wie ich den Bedarf messe, Grenzen setze und Wartezeiten des Dienstes respektiere.
Das Dashboard zeigt, dass die Bereitstellung kaum ausgelastet ist, und trotzdem kommen reihenweise 429-Antworten zurück. Wer dann das Kontingent erhöht, bezahlt für ein Problem, das im eigenen Request steckt. Hier steht, wie Azure OpenAI das Limit tatsächlich rechnet und was ich an meinen Aufrufen geändert habe.
Das Symptom: Drosselung ohne erkennbare Last
Das Muster ist immer ähnlich. Eine Anwendung schickt kurze Anfragen, etwa eine Klassifikation oder eine Zusammenfassung von ein paar Absätzen. Die tatsächlich verbrauchten Tokens liegen weit unter dem Kontingent der Bereitstellung. Trotzdem antwortet der Dienst bei mehreren gleichzeitigen Anfragen mit HTTP 429 und einem Hinweis, man möge es später erneut versuchen.
Der naheliegende Reflex ist, die Tokens pro Minute hochzusetzen oder eine zweite Region dazuzunehmen. Das hilft manchmal, verdeckt aber die Ursache. Und Kontingent ist in den meisten Abonnements nicht unbegrenzt verfügbar.
Wie das Limit gerechnet wird
Der entscheidende Punkt steht in der Dokumentation zu Kontingenten, wird aber gern überlesen: Die Ratenbegrenzung wird beim Eingang der Anfrage geschätzt, nicht erst nach der Antwort abgerechnet. In diese Schätzung gehen die Prompt-Tokens ein und zusätzlich der Wert von max_tokens, also die Länge, die die Antwort höchstens haben darf. Wie lang die Antwort am Ende tatsächlich wird, spielt für die Schätzung keine Rolle.
Dazu kommen zwei weitere Eigenheiten, die ebenfalls dokumentiert sind:
- Neben den Tokens pro Minute gibt es ein Limit für Anfragen pro Minute, das aus dem Token-Kontingent abgeleitet wird. Wer viele winzige Anfragen schickt, läuft eher in dieses zweite Limit.
- Geprüft wird nicht über die volle Minute, sondern über kurze Zeitfenster von wenigen Sekunden. Ein Stoß von Anfragen zur selben Sekunde kann deshalb gedrosselt werden, obwohl der Minutendurchschnitt harmlos aussieht.
Ein Rechenbeispiel, ausdrücklich nur zur Veranschaulichung: Eine Klassifikation mit 300 Prompt-Tokens und einer Antwort von 20 Tokens verbraucht wenig. Steht im Code aber max_tokens=4000, weil das irgendwann als sicherer Standard eingetragen wurde, zählt die Schätzung pro Anfrage über 4000 Tokens. Die Bereitstellung wirkt dann für die Drosselung mehr als zehnmal so voll, wie sie ist.
Das Vorgehen: erst messen, dann begrenzen
Ich gehe in drei Schritten vor, und keiner davon braucht mehr Kontingent.
Erstens lese ich aus den Antworten das Feld usage aus und schaue, wie lang die Antworten je Aufgabe wirklich werden. Das sind die Zahlen, an denen sich max_tokens orientieren sollte, nicht ein Bauchgefühl.
Zweitens setze ich max_tokens je Aufgabe knapp über diesen gemessenen Bedarf. Eine Ja-Nein-Klassifikation braucht keine viertausend Tokens Spielraum. Wenn eine Antwort doch abgeschnitten wird, zeigt das finish_reason mit dem Wert length an, und genau das prüfe ich, statt vorsorglich hohe Grenzen zu setzen.
Drittens behandle ich 429 als normalen Betriebszustand und nicht als Fehler. Der Dienst schickt in der Antwort Kopfzeilen mit, die sagen, wie lange gewartet werden soll. Die lese ich aus, statt fest eine Sekunde zu schlafen oder sofort erneut zu senden.
import random
import time
import httpx
def wartezeit(antwort: httpx.Response, versuch: int) -> float:
# Vorgabe des Dienstes hat Vorrang vor der eigenen Schätzung
ms = antwort.headers.get("retry-after-ms")
if ms:
return float(ms) / 1000
s = antwort.headers.get("retry-after")
if s and s.isdigit():
return float(s)
return min(30.0, 2 ** versuch) + random.uniform(0, 1)
def senden(client: httpx.Client, url: str, body: dict, versuche: int = 5) -> dict:
for versuch in range(versuche):
antwort = client.post(url, json=body)
if antwort.status_code != 429:
antwort.raise_for_status()
return antwort.json()
time.sleep(wartezeit(antwort, versuch))
raise RuntimeError("Kontingent dauerhaft erschöpft")Der Zufallsanteil am Ende der Wartezeit ist kein Schmuck. Ohne ihn wachen alle gedrosselten Anfragen gleichzeitig wieder auf und erzeugen genau den Stoß, der die Drosselung ausgelöst hat. Die offiziellen SDKs bringen übrigens eigene Wiederholungslogik mit. Wer sie nutzt, sollte nachsehen, wie viele Versuche dort eingestellt sind, bevor er eine zweite Schleife darumlegt. Sonst multiplizieren sich die Versuche still.
Was es bringt und wo die Grenze liegt
Das Ergebnis ist unspektakulär, und das ist gut so: Die gleiche Bereitstellung trägt mehr gleichzeitige Anfragen, weil die Schätzung näher an der Wirklichkeit liegt. Die verbleibenden 429-Antworten werden abgefangen, statt beim Nutzer als Fehlermeldung anzukommen. Wie groß der Gewinn ausfällt, hängt vollständig davon ab, wie weit max_tokens vorher danebenlag. Eine allgemeine Zahl gebe ich deshalb bewusst nicht an.
Die Grenze ist ebenso klar. Wer dauerhaft mehr Tokens verbraucht, als das Kontingent hergibt, löst das nicht mit Wiederholungen, sondern verschiebt das Problem nur in die Wartezeit. Dann sind die üblichen Wege dran: Kontingent erhöhen, Last über mehrere Bereitstellungen verteilen oder für planbare Dauerlast bereitgestellten Durchsatz prüfen. Diese Entscheidung sollte man aber auf Basis der gemessenen usage-Werte treffen und nicht auf Basis einer Schätzung, die man sich mit einem zu großzügigen Parameter selbst verdorben hat.
Ein Nebeneffekt, den ich nicht erwartet hatte: Wer max_tokens je Aufgabe festlegen muss, denkt zwangsläufig darüber nach, wie lang die Antwort eigentlich sein soll. Das schärft oft auch den Prompt.
Fazit
Ein 429 bei scheinbar halbleerem Kontingent ist meistens kein Kapazitätsproblem, sondern ein Schätzproblem. Die Drosselung rechnet mit dem, was eine Antwort höchstens kosten darf, nicht mit dem, was sie kostet. Also: echten Bedarf aus usage messen, max_tokens passend setzen, Wartezeiten des Dienstes respektieren und erst dann über mehr Kontingent reden. Das ist billiger als jede Kontingenterhöhung und lässt sich an einem Nachmittag umsetzen.
- #Azure OpenAI
- #Rate Limits
- #HTTP 429
- #max_tokens
- #Retry
- #Kontingent
Weiterlesen
Zeichen sind keine Token: wie ein Embedding-Stapel an 300.000 Token scheiterte
13. November 2026 · RAG, KI-Agenten, Engineering · 5 Min. Lesezeit
Woran RAG-Projekte scheitern: vier Fehler jenseits der Technik
13. November 2026 · RAG, Security, Engineering · 5 Min. Lesezeit
Reranking erklärt: Wie die besten Treffer in den Prompt kommen
12. November 2026 · RAG, Engineering · 4 Min. Lesezeit