Azure Troubleshooting

Taming Azure OpenAI 429s: TPM vs RPM Limits, Quota Math, Retry-After Headers, and Backoff That Works

Your app was fine in the demo. Then real traffic arrived, and now every few seconds a call to your Azure OpenAI chat deployment comes back 429 Too Many Requests with a terse JSON body and a Retry-After: 23 header. Some requests sail through; others bounce. The model didn’t change, the prompt didn’t change, your code didn’t change — the rate changed. This is the most common production failure on Azure OpenAI, and it is maddening because a 429 is not an error in your code or the model’s output. It is the platform’s flow-control valve telling you, politely, that you are asking faster than your quota allows, and to slow down and try again.

A 429 has two distinct triggers, and conflating them is why teams stay stuck. Azure OpenAI meters every Standard and Global Standard deployment on two simultaneous limits: TPM (tokens per minute) — the tokens (prompt + completion) you may process in a rolling window — and RPM (requests per minute) — how many calls you may make regardless of size. Hit either ceiling and you get a 429. A flood of tiny prompts exhausts RPM while TPM sits idle; a handful of huge RAG calls exhausts TPM while RPM sits idle. The fix for one is not the fix for the other, so the first job in any incident is to learn which limit you hit — and the platform tells you, in headers most people never read.

This is the diagnostic playbook for that. You will learn the quota math (how TPM is assigned, how RPM is derived from it, why raising one moves the other), how to read the Retry-After and x-ratelimit-* headers for live headroom, and how to build retry logic that honours the server’s Retry-After instead of guessing — because naive fixed-interval retries make a storm worse. We cover the deployment types (Standard, Global Standard, Data Zone, Provisioned PTU), plus spillover, batching and caching, each with the exact az CLI, portal path or KQL to confirm it and the precise fix.

By the end you will stop guessing. When 429s spike you will know within ninety seconds whether you blew the token budget or the request budget, whether a bursty client is starving everyone, whether you need more quota (and whether it is even available in your region), or whether the real fix is backoff, batching, a second deployment, or a jump to Provisioned Throughput — the difference between a five-minute mitigation and a day lost to scaling the wrong dial.

What problem this solves

Azure OpenAI sells capacity in an abstract unit — tokens per minute — and enforces it invisibly until you cross the line, then returns a 429 with almost no human-readable context. The status code deliberately tells you little; the useful information lives in HTTP headers and Azure Monitor metrics, and if you do not know which header maps to which limit you will burn an afternoon scaling quota that was never the bottleneck.

What breaks without this knowledge: an engineer sees 429s and immediately requests a TPM quota increase (a multi-day approval) when the real problem was a retry loop hammering RPM, or a missing max_tokens letting the model reserve far more budget than each call uses. Or they retry immediately on every 429, turning a brief throttle into a self-inflicted denial-of-service. Or they discover, mid-incident, that the region has no spare quota and the increase will take days, when a Global Standard deployment would have absorbed the burst immediately.

Who hits this: essentially everyone running Azure OpenAI under real load. It bites hardest on chat and RAG apps (large prompts eat TPM fast), batch jobs firing thousands of small calls (RPM-bound), apps with no client-side throttling (retry storms), multi-tenant platforms where one tenant starves the rest, and anyone who provisioned a small default quota and never checked it against projected traffic. The fix is rarely “ask for more tokens” — it is “find which budget you blew, honour the backoff, stop wasting tokens, and put capacity where the traffic is.”

To frame the field before the deep dive, here is every way a 429 reaches you, the first place to look, and the most common single cause:

429 trigger First place to look Most common single cause
TPM exceeded x-ratelimit-remaining-tokens header Big RAG prompts + no max_tokens cap
RPM exceeded x-ratelimit-remaining-requests header Batch loop / aggressive retries
Retry storm (self-inflicted) App logs: retry cadence vs Retry-After Fixed-interval / no-backoff retry
No spare quota in region az cognitiveservices ... deployment show Default/low TPM never raised
Subscription quota exhausted Quotas blade / usage list Many deployments share one regional pool
PTU demand saturation azure-openai-ptu-utilization metric PTU sized below peak concurrency

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should already understand the basics: an Azure OpenAI resource (a kind of Azure AI Services / Cognitive Services account) hosts one or more model deployments — a named instance of a base model like gpt-4o or gpt-4o-mini that you call by deployment name. You call it over REST or the SDKs (Python/JS/.NET) with an API key or Microsoft Entra ID token, and requests and responses are measured in tokens (~4 characters or ~0.75 words each in English). Familiarity with HTTP status codes and basic retry concepts helps.

If tokens and pricing are still fuzzy, read Azure OpenAI tokens, context windows & pricing model explained first — the quota math assumes you know prompt + completion both count. The deployment-type choices are covered in Azure OpenAI deployment types: Standard, Global & Provisioned explained, the natural companion; and Deploy your first Azure OpenAI chat model (REST + SDK) gets you a deployment to load-test.

This sits in the AI/ML Observability & Reliability track and pairs with Azure Monitor & Application Insights for observability, where the token-usage and 429 metrics live. RAG workloads — the largest prompts and fastest TPM burn — are covered in Azure OpenAI “on your data”: RAG grounding & citations.

A quick map of who owns what during a 429 incident, so you call the right person fast:

Layer What lives here Who usually owns it 429 classes it can cause
Client app / SDK Retry policy, max_tokens, concurrency App / dev team Retry storms, RPM burn from tiny calls
Deployment config Assigned TPM, model, deployment type AI platform owner TPM/RPM ceiling hit at assigned quota
Resource / region Total regional token pool, model availability Cloud platform / FinOps Subscription-quota exhaustion, no spare quota
Provisioned capacity PTU count, spillover config AI platform + finance PTU saturation under peak concurrency
Front door (APIM, gateway) Per-tenant throttling, load balancing Network / platform Fair-use throttle (your own 429s, by design)

Core concepts

Five mental models make every diagnosis obvious:

A 429 is flow control, not a failure. Unlike a 500 (the platform broke) or a 400 (your request was malformed), a 429 Too Many Requests means the request was valid but exceeded your assigned rate. The correct response is to wait and retry, not fix code or escalate — and the platform tells you how long to wait, in the Retry-After header. Treating 429 as fatal is the common over-reaction; “retry forever, immediately” is the under-reaction. The right behaviour is in between: back off, respect the header, retry a bounded number of times.

There are two limits, enforced at once. Every Standard-family deployment carries a TPM ceiling and an RPM ceiling, and you are throttled the instant you cross either. They are not independent dials — you assign TPM, and RPM is derived from it. This is the most important fact in the article: a 429 is ambiguous until you know which budget it charged against, and the two have opposite fixes.

The platform estimates your token cost before the call. It cannot know your completion length in advance, so at admission it estimates the cost — prompt tokens plus a reservation based on max_tokens (or the model’s default if you omit it). If that estimate would overflow the rolling TPM window, you get a 429 before the model even runs. This is why omitting max_tokens causes spurious 429s: the platform reserves a large default per call, so far fewer calls “fit” than your actual usage warrants.

Quota is assigned per deployment but drawn from a regional pool. Each deployment gets an assigned TPM from a per-region, per-model-family subscription quota. You can 429 because this deployment is at its assigned TPM, or because the region’s total quota for that family is exhausted across all your deployments. The first is fixed by raising this deployment’s TPM (if regional headroom exists); the second needs a quota-increase request or a different region.

Deployment type changes the capacity model entirely. Standard and Global Standard are pay-per-token with TPM/RPM throttling — you share a pool and 429 when you exceed your slice. Provisioned (PTU) is reserved capacity: you buy Provisioned Throughput Units, get guaranteed throughput, and are throttled only when your own units saturate. Choosing the type is choosing your throttling model: burstable-but-shared, or reserved-but-fixed.

The vocabulary in one table

Before the deep sections, pin down every moving part. The glossary at the end repeats these for lookup; this table is the mental model side by side:

Concept One-line definition Where it lives Why it matters to 429
Token ~4 chars of text; prompt + completion both count Request + response The unit TPM meters; big prompts burn it fast
TPM Tokens per minute you may process Deployment quota One of the two limits; exceed → 429
RPM Requests per minute you may make Derived from TPM The other limit; tiny calls exhaust it
Quota Regional, per-model-family token budget Subscription / region Caps total TPM you can assign
Retry-After Seconds the server wants you to wait 429 response header The correct backoff value to honour
x-ratelimit-remaining-tokens Tokens left in the current window Response header Live TPM headroom
x-ratelimit-remaining-requests Requests left in the current window Response header Live RPM headroom
max_tokens Cap on completion length per call Request body Bounds the token reservation; prevents spurious 429
Standard / Global Standard Pay-per-token, shared-pool deployment Deployment type TPM/RPM throttling applies
PTU Provisioned Throughput Unit (reserved capacity) Provisioned deployment No shared-pool 429; saturate-your-own only
Spillover Overflow PTU traffic to a Standard deployment PTU + Standard pair Absorbs bursts above provisioned capacity
Batch API Async, 24-h, cheaper, separate quota Batch deployment Moves bulk work off your live TPM

The 429 and its headers: reading the truth

Before the quota math, learn to read the response — it already tells you almost everything. A throttled call returns HTTP 429 with a small JSON body and, crucially, headers that name the limit you hit and the headroom you have left.

A typical 429 body:

{
  "error": {
    "code": "429",
    "message": "Requests to the ChatCompletions_Create Operation under Azure OpenAI API version 2024-10-21 have exceeded call rate limit of your current OpenAI S0 pricing tier. Please retry after 23 seconds. Please contact Azure support service if you would like to further increase the default rate limit."
  }
}

The message itself says “retry after 23 seconds” — and the same value appears in the header. The headers are the machine-readable contract:

Header What it tells you Present on How to use it
Retry-After Seconds to wait before retrying 429 responses The exact backoff to honour — wait at least this long
x-ratelimit-limit-tokens Your TPM ceiling for this deployment Most responses Confirms assigned TPM without a portal trip
x-ratelimit-remaining-tokens Tokens left in the current window Most responses If near 0 when you 429 → it was a TPM hit
x-ratelimit-limit-requests Your RPM ceiling for this deployment Most responses Confirms derived RPM
x-ratelimit-remaining-requests Requests left in the current window Most responses If near 0 when you 429 → it was an RPM hit
x-ms-region The region that actually served (Global) Global deployments Confirms where a Global call landed
apim-request-id / x-request-id Correlation ID for this call All responses Quote it in a support ticket

The diagnostic move is decisive. When a 429 arrives, read both remaining headers from the last successful response (or the 429 itself): if remaining-requests hit zero first you are RPM-bound (too many calls — batch, throttle concurrency, fix the retry policy); if remaining-tokens hit zero first you are TPM-bound (too many tokens — trim prompts, cap max_tokens, raise TPM, cache). If both are near zero you have sustained high load (add quota or go PTU); if neither is present and 429s repeat immediately, suspect regional-quota exhaustion or a deployment still warming. That one observation routes you to the correct half of every fix below.

Capture these headers in any language. In Python with the OpenAI SDK, use the raw-response accessor:

from openai import AzureOpenAI

client = AzureOpenAI(
    api_version="2024-10-21",
    azure_endpoint="https://aoai-shop-prod.openai.azure.com",
    api_key="<key>",  # prefer Entra ID / managed identity in prod
)

raw = client.chat.completions.with_raw_response.create(
    model="gpt-4o-mini",            # your DEPLOYMENT name, not the base model id
    max_tokens=400,                  # cap the completion → smaller token reservation
    messages=[{"role": "user", "content": "Summarise this ticket: ..."}],
)
print("remaining tokens:", raw.headers.get("x-ratelimit-remaining-tokens"))
print("remaining requests:", raw.headers.get("x-ratelimit-remaining-requests"))
completion = raw.parse()

Log those two remaining values on every call (or sample them) and a 429 is never a mystery — you see which was trending to zero before the throttle.

The quota math: how TPM, RPM and the token estimate fit together

This is the section that ends the guesswork. Three numbers govern every 429, tied by fixed rules.

TPM is what you assign; RPM is derived

You do not set RPM. You assign TPM, and Azure OpenAI derives the RPM from it at a fixed ratio of 6 RPM per 1,000 TPM: 60,000 TPM gets 360 RPM; 10,000 TPM gets 60 RPM; 1,000 TPM gets just 6 RPM. This coupling is why a “request limit” problem is sometimes solved by raising the token limit — more TPM buys proportionally more RPM. The ratio in practice, with an illustrative (not contractual) sense of what each tier supports:

Assigned TPM Derived RPM (6 / 1,000 TPM) Roughly supports
1,000 6 demo only — RPM-starved instantly
10,000 60 ~1 small call/sec, a few medium calls/min
30,000 180 ~3 small calls/sec, ~10–20 RAG calls/min
60,000 360 ~6 small calls/sec, ~20–40 RAG calls/min
100,000 600 ~10 small calls/sec, ~40–60 RAG calls/min

The shape is the lesson: at low TPM you are RPM-starved long before tokens matter; at high TPM with big prompts you are TPM-starved first.

The pre-call token estimate (why max_tokens matters so much)

Azure OpenAI admits or rejects a call before running the model, using an estimate of its token cost: roughly prompt tokens + the completion reservation, where the reservation is your max_tokens value — or, if you omit it, a large model default. It checks that estimate against your remaining TPM in the rolling 60-second window; if it would overflow, you get a 429 even though the actual completion might have been tiny.

The consequence is counter-intuitive: two apps with identical real token usage can have wildly different 429 rates purely because one caps max_tokens and the other does not — the uncapped app reserves the large default per call, fills its TPM window with reservations, and 429s long before its real usage justifies it.

Scenario Prompt tokens max_tokens set? Per-call TPM reservation Calls that “fit” in 60k TPM/min
Capped, lean 500 Yes, 300 ~800 ~75
Capped, generous 500 Yes, 2,000 ~2,500 ~24
Uncapped 500 No ~500 + large default (e.g. 4k–16k) ~3–10
Big RAG, capped 6,000 Yes, 800 ~6,800 ~8

The lesson: always set max_tokens to the smallest value you tolerate. It is the cheapest 429 fix there is — a one-line change that can multiply effective throughput.

Quota: per-deployment assignment from a regional pool

Each deployment’s assigned TPM is drawn from a per-subscription, per-region, per-model-family quota. New subscriptions get default quotas that are often small, and the total across all deployments of that family in that region is capped by the regional quota. Two failure modes follow:

Quota failure What you observe How to confirm Fix
Deployment at its assigned TPM 429s on one deployment; others fine az ... deployment show → capacity vs usage Raise this deployment’s TPM (if regional headroom exists)
Regional/model quota exhausted Cannot raise TPM or create a deployment; “quota exceeded” Quotas blade (assigned ≈ limit) Request a quota increase, or use another region / Global
Default quota too low for launch Even modest load 429s from day one Quotas blade shows tiny default Request increase early; it is not instant

Confirm a deployment’s assigned capacity and current usage from the CLI:

# Show a deployment's assigned capacity (in thousands of TPM) and model
az cognitiveservices account deployment show \
  --name aoai-shop-prod --resource-group rg-shop-prod \
  --deployment-name gpt-4o-mini \
  --query "{model:properties.model.name, sku:sku.name, capacity:sku.capacity}" -o table

And the regional usage-vs-limit (the subscription quota) for the account’s location:

# List quota usage for the region — assigned vs limit per model family
az cognitiveservices usage list --location centralindia \
  --query "[?contains(name.value, 'OpenAI')].{name:name.value, used:currentValue, limit:limit}" -o table

When you create or update a deployment, the capacity you set is the TPM in units of 1,000 (--sku-capacity 60 = 60,000 TPM). Setting it via Bicep makes the quota assignment reviewable as code:

resource deployment 'Microsoft.CognitiveServices/accounts/deployments@2024-10-01' = {
  parent: aoaiAccount
  name: 'gpt-4o-mini'
  sku: {
    name: 'GlobalStandard'   // or 'Standard'
    capacity: 60             // 60 => 60,000 TPM; RPM derived at 6/1000 => 360 RPM
  }
  properties: {
    model: {
      format: 'OpenAI'
      name: 'gpt-4o-mini'
      version: '2024-07-18'
    }
    raiPolicyName: 'Microsoft.DefaultV2'
  }
}
# Same via CLI — bump assigned capacity to 60k TPM
az cognitiveservices account deployment create \
  --name aoai-shop-prod --resource-group rg-shop-prod \
  --deployment-name gpt-4o-mini \
  --model-name gpt-4o-mini --model-version "2024-07-18" --model-format OpenAI \
  --sku-name GlobalStandard --sku-capacity 60

Raising --sku-capacity only works if the region has unassigned quota left; otherwise the command fails with a quota error and you need a quota-increase request (from the Quotas blade or the Azure OpenAI request form) — which is not instant, so size early.

Deployment types and what each does to your limits

The 429 you get — and whether you can get one at all — depends on the deployment type. Pick the one matching your problem: burst, latency, residency, or guaranteed throughput.

Deployment type Capacity model 429 behaviour Best for Trade-off
Standard (regional) Pay-per-token, regional shared pool TPM/RPM throttling; data stays in region Single-region apps with data-residency needs Smaller shared pool → 429s earlier under burst
Global Standard Pay-per-token, global shared pool TPM/RPM throttling but a far larger pool; request may process in any region Highest throughput, best price, burst absorption Data may process outside your region
Data Zone Standard Pay-per-token, multi-region within a data boundary (e.g. EU, US) Throttling with a larger-than-regional pool, residency within the zone Need more headroom than regional but a residency boundary Slightly less headroom than fully Global
Provisioned (PTU) Reserved units; guaranteed throughput 429 only when your PTUs saturate; predictable latency Steady high volume, latency-sensitive, SLA-bound Pay for reserved capacity even when idle
Batch Async, 24-h turnaround, separate quota Separate enqueued-tokens quota; ~50% cheaper Bulk/offline enrichment, embeddings at scale Not for real-time; results within 24 h

Standard vs Global vs PTU, briefly

For bursty 429s (fine at baseline, throttled during spikes), the cheapest structural fix is usually Global Standard — a much larger pool, often cheaper per token, processing in whichever region has capacity. The cost is data-residency: a Global request may be processed outside your resource’s region (data is not stored, but processed elsewhere). With a residency requirement, Data Zone Standard keeps processing within a boundary (EU/US) while still giving more headroom than one region.

For constant 429s — sustained demand beyond any reasonable shared-pool slice, or a need for predictable latency — the answer is Provisioned Throughput: buy PTUs (reserved capacity), within which throughput and latency are guaranteed and shared-pool 429s do not apply; you throttle only past your own capacity. The trade is cost — PTUs bill for reserved capacity (hourly, or far cheaper via monthly/annual reservations) whether used or not, paying off only at steady high utilisation. You need not go all-or-nothing: spillover overflows a provisioned deployment’s excess to a paired Standard deployment, so PTU handles the steady base and bursts spill to pay-per-token — the pattern for “mostly steady with occasional spikes.”

If your 429 pattern is… It’s probably… Best deployment move
Spiky, fine at baseline Burst exceeding a small regional slice Global Standard (bigger pool)
Constant at all hours Sustained demand > shared-pool slice Provisioned (PTU)
Steady base + occasional spikes Right-sized base, unhandled bursts PTU + spillover to Standard
Bulk/offline, not real-time Live quota wasted on batch work Batch API (separate quota)
Need residency + more headroom Region too small, can’t go Global Data Zone Standard

Retry and backoff that actually works

Getting a 429 is normal; how you retry decides whether the incident self-heals or spirals. The cardinal rule: honour the server’s Retry-After and add exponential backoff with jitter. The anti-patterns are immediate retry (hammers the same wall), fixed-interval retry (synchronises clients into a thundering herd), and unbounded retry (never lets the queue drain).

Retry strategy What it does 429 outcome Verdict
Immediate retry Re-fires the instant a 429 returns Another request against the exhausted limit → storm Never
Fixed interval (e.g. every 1 s) Retries on a constant clock All clients sync up → thundering herd Poor
Exponential backoff (no jitter) Doubles the wait each attempt Better, but clients still sync on the same curve OK
Exponential backoff + jitter Doubles the wait, randomised Spreads retries; queue drains smoothly Good
Honour Retry-After + backoff + jitter, bounded Waits at least the server’s value, then backs off, capped attempts Respects the platform; predictable recovery Best

The decision logic for a single call: on a 429, read Retry-After; wait at least that long plus a small random jitter; retry; if it 429s again, double the wait (capped at, say, 60 s); stop after a bounded number of attempts (e.g. 5) and surface a clean error rather than retrying forever.

A correct implementation in Python (honouring Retry-After, exponential backoff, jitter, bounded attempts):

import random, time
from openai import AzureOpenAI, RateLimitError

client = AzureOpenAI(api_version="2024-10-21",
                     azure_endpoint="https://aoai-shop-prod.openai.azure.com",
                     api_key="<key>")

def chat_with_backoff(messages, max_attempts=5):
    for attempt in range(max_attempts):
        try:
            return client.chat.completions.create(
                model="gpt-4o-mini", max_tokens=400, messages=messages)
        except RateLimitError as e:
            # Prefer the server's Retry-After; fall back to exponential backoff.
            retry_after = None
            if e.response is not None:
                retry_after = e.response.headers.get("retry-after")
            wait = float(retry_after) if retry_after else min(2 ** attempt, 60)
            wait += random.uniform(0, 1)          # jitter spreads the herd
            if attempt == max_attempts - 1:
                raise                              # bounded: give up cleanly
            time.sleep(wait)

The OpenAI Python SDK already retries on 429 a couple of times by default and honours Retry-After; the snippet above makes the policy explicit and bounded so you control the cap, jitter and failure surface. For .NET, the Azure.AI.OpenAI client uses the standard Azure SDK retry pipeline, which honours Retry-After automatically — but still set a max-retry and a circuit-breaker so a sustained outage fails fast instead of stacking retries.

Two backoff gotchas bite even careful teams: retrying a streaming call from scratch re-sends the whole prompt (more tokens, more TPM burn), so bound stream retries tightly; and retries multiply under concurrency (10 concurrent 429s become 10 simultaneous retries into the same wall), so add a concurrency limiter upstream. The deeper point: backoff handles transient throttling gracefully, but if you 429 constantly retries cannot save you — you are over-demand for your quota, and the fix is more capacity (quota, Global, PTU) or less demand (caching, batching, smaller prompts), not a cleverer retry loop.

Cutting token demand at the source

Before buying more capacity, stop wasting what you have — every token you do not send is TPM you do not consume.

Technique What it saves Effort When it shines
Set a tight max_tokens Shrinks the per-call reservation → far more calls fit Trivial Always — biggest cheap win
Trim the prompt / context Fewer prompt tokens per call Low–Medium Long system prompts, fat RAG context
Cache identical responses Skips the call entirely for repeats Medium FAQ-style or deterministic prompts
Use a smaller model gpt-4o-mini vs gpt-4o — cheaper, higher limits Low Tasks that don’t need the flagship
Batch API for bulk work Moves offline work off your live TPM Medium Embeddings, enrichment, nightly jobs
Tighten RAG retrieval (fewer/shorter chunks) Cuts the largest token contributor Medium RAG apps burning TPM on context

A response cache (e.g. Redis) in front of deterministic or repeated prompts removes load entirely — a cache hit is zero tokens and zero RPM; see Azure Cache for Redis basics & caching patterns. For bulk work — embeddings for a corpus, classifying a backlog — the Batch API has a separate quota at roughly half the price, so it saves money and keeps your live TPM free.

Architecture at a glance

The diagram traces a request as it flows toward an Azure OpenAI deployment and marks where each 429 class bites. Read it left to right. A client app issues a chat/RAG call; before it reaches the model, a sensible architecture passes it through a gateway / throttle layer (often Azure API Management or a custom concurrency limiter) that enforces per-tenant fairness and queues bursts so one client cannot starve the rest. The call then hits the Azure OpenAI resource and its model deployment, where the TPM and RPM meters decide — using the pre-call token estimate — whether to admit or 429 it. Those meters draw from the regional quota pool; when exhausted you get the 429 with a Retry-After to honour.

Notice the two capacity paths on the right: a Standard / Global Standard deployment shares a large pool and absorbs bursts up to your slice, then throttles; a Provisioned (PTU) deployment reserves capacity for guaranteed throughput, with spillover routing overflow to a paired Standard deployment instead of failing. The numbered badges mark the four places a 429 originates — RPM and TPM exhaustion at the meter, a retry storm from the client, and regional-quota exhaustion in the pool — each mapped in the legend to its confirming header and fix. The whole method is in the picture: localise the 429 to a badge, read the header, apply the fix.

Azure OpenAI request flow from a client app through an API Management gateway and concurrency throttle into the Azure OpenAI resource and model deployment, where TPM and RPM meters use a pre-call token estimate to admit or return 429 Too Many Requests with a Retry-After header, drawing capacity from a regional quota pool, with one path to a Standard or Global Standard shared-pool deployment and another to a Provisioned PTU deployment with spillover to Standard — numbered badges mark RPM exhaustion, TPM exhaustion, a self-inflicted retry storm from the client, and regional-quota exhaustion, each mapped in the legend to the confirming header and the fix

Real-world scenario

Finlytics runs a customer-support copilot on Azure OpenAI: a RAG chat app over their knowledge base, on a gpt-4o Standard deployment in Central India assigned 30,000 TPM (so 180 RPM derived). At launch, traffic was light and smooth. The platform team is three engineers; monthly Azure OpenAI spend was about ₹22,000.

The incident began the morning a large customer went live and support volume tripled. From 09:40 the app threw errors to users — the UI showed “the assistant is unavailable” — and logs were full of 429 Too Many Requests. The on-call engineer’s first reflex was to file a TPM quota-increase request (a multi-day approval) and “wait it out.” Meanwhile the app kept failing, because the second reflex was worse: the client retried 429s immediately, so every throttle spawned a fresh request against the same exhausted limit. The deployment was under self-inflicted DoS, and remaining headroom never recovered.

The breakthrough came from reading the headers. The engineer added x-ratelimit-remaining-tokens and x-ratelimit-remaining-requests to the structured logs and replayed traffic. The picture was unambiguous: remaining-tokens was hitting zero, remaining-requests was healthy — a TPM problem, not an RPM one, so the quota request was not the urgent fix. Two TPM amplifiers stood out: the RAG layer stuffed the top 12 chunks (≈ 9,000 prompt tokens) into every prompt, and the chat calls set no max_tokens, so the platform reserved gpt-4o’s large default per call. Between fat prompts and huge reservations, 30k TPM was exhausted by a few dozen calls a minute — far below real demand.

The fixes landed in three waves. Immediately: add backoff that honours Retry-After with jitter and a 5-attempt cap, stopping the retry storm so the queue drained — user error rate dropped sharply within minutes, before any capacity change. That morning: set max_tokens=600 (cutting the reservation by an order of magnitude) and trim RAG retrieval from 12 chunks to the top 4 reranked (≈ 3,000 prompt tokens), roughly quadrupling the real traffic that fit in 30k TPM. That week: because baseline demand had genuinely grown, they migrated to Global Standard for a larger burst pool (and lower per-token cost) and added a Redis cache for repeated FAQ answers, removing ~25% of calls entirely.

The next spike — larger than the one that caused the outage — passed without a single user-facing error, and the quota increase (finally approved) was no longer needed. Spend settled at ₹19,500, lower than before, thanks to gpt-4o-mini on the cheaper RAG paths and the cache. The lesson on the wall: “A 429 is a question — which budget did you blow? Read the headers before you scale anything.”

The incident as a timeline, because the order of moves is the lesson:

Time Symptom Action Effect What it should have been
09:40 429s, users see failures File a TPM quota increase Multi-day; no relief now Ask: TPM or RPM? Read headers
09:45 429s worsen (client retries immediately) Self-inflicted storm; never recovers Add backoff first
10:10 Still failing Log remaining-tokens / remaining-requests Tokens → 0, requests healthy = TPM This was the breakthrough
10:25 Root cause clear Add Retry-After backoff + jitter, 5-attempt cap Storm stops; queue drains Correct first move
10:40 Mitigating max_tokens=600 + RAG 12→4 chunks ~4× more real traffic fits in 30k TPM The real same-day fix
+3 days Fixed Migrate to Global Standard + Redis cache Bursts absorbed; ~25% calls cached Capacity where demand is

Advantages and disadvantages

The TPM/RPM shared-quota model both causes this class of problem and makes it tractable. Weigh it honestly:

Advantages (why this model helps you) Disadvantages (why it bites)
Pay-per-token: you pay only for what you process, no reserved-capacity bill A shared pool means bursts can exceed your slice and 429 with no warning
Response headers expose live headroom (remaining-tokens/-requests) — you are never blind Most teams never read those headers, so 429s feel random
Retry-After makes correct backoff trivial — the server tells you the wait Naive retries amplify the problem into a self-inflicted storm
Quota is adjustable and Global/Data-Zone variants add huge headroom on demand Default quotas are often tiny; increases are not instant — you must size early
Deployment types (Standard/Global/PTU/Batch) let you match capacity to the workload Choosing wrong (e.g. Standard for sustained high volume) guarantees chronic 429s
max_tokens and prompt trimming multiply effective throughput for free Omitting max_tokens silently reserves a large default and 429s you early
PTU offers guaranteed throughput + predictable latency when you need it PTU bills for reserved capacity whether or not you use it — only pays off at steady load

The model is right for apps that want elastic, pay-per-use access to frontier models and can tolerate occasional, well-handled throttling. It bites hardest on workloads that are bursty without backoff, token-wasteful, or sustained-high-volume on a small Standard slice. Every disadvantage is manageable — read the headers, honour Retry-After, cap tokens, put capacity where the traffic is.

Hands-on lab

Reproduce a 429 deliberately by squeezing a deployment to a tiny TPM, watch the headers tell you which limit you hit, then fix it with backoff and a max_tokens cap. Free-tier-friendly in spirit (you pay only per token for a few small calls; delete at the end). Run in Cloud Shell (Bash) plus a small Python script.

Step 1 — Variables and a resource (skip if you already have one).

RG=rg-aoai-lab
LOC=swedencentral            # pick a region with model availability + spare quota
ACC=aoai-lab-$RANDOM         # globally-unique resource name
az group create -n $RG -l $LOC -o table
az cognitiveservices account create -n $ACC -g $RG -l $LOC \
  --kind OpenAI --sku S0 --custom-domain $ACC -o table

Step 2 — Deploy a model with an intentionally tiny TPM (1k) to force throttling fast.

# capacity 1 => 1,000 TPM => 6 RPM derived. Deliberately small so we 429 quickly.
az cognitiveservices account deployment create -n $ACC -g $RG \
  --deployment-name gpt-4o-mini \
  --model-name gpt-4o-mini --model-version "2024-07-18" --model-format OpenAI \
  --sku-name Standard --sku-capacity 1

Expected: a deployment with sku.capacity = 1. Confirm:

az cognitiveservices account deployment show -n $ACC -g $RG \
  --deployment-name gpt-4o-mini --query "{sku:sku.name, capacity:sku.capacity}" -o table

Step 3 — Grab the endpoint and key.

ENDPOINT=$(az cognitiveservices account show -n $ACC -g $RG --query properties.endpoint -o tsv)
KEY=$(az cognitiveservices account keys list -n $ACC -g $RG --query key1 -o tsv)
echo "$ENDPOINT"

Step 4 — Fire a burst with NO max_tokens and watch the headers. Save as burst.py, set AZURE_OPENAI_ENDPOINT/AZURE_OPENAI_KEY, and run it:

import os, time
from openai import AzureOpenAI, RateLimitError

client = AzureOpenAI(api_version="2024-10-21",
                     azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
                     api_key=os.environ["AZURE_OPENAI_KEY"])

for i in range(12):
    try:
        r = client.chat.completions.with_raw_response.create(
            model="gpt-4o-mini",                 # no max_tokens => large reservation
            messages=[{"role": "user", "content": "Write a short paragraph about Azure."}])
        print(i, "OK  rem-tokens:", r.headers.get("x-ratelimit-remaining-tokens"),
                 "rem-requests:", r.headers.get("x-ratelimit-remaining-requests"))
    except RateLimitError as e:
        ra = e.response.headers.get("retry-after") if e.response else None
        print(i, "429  Retry-After:", ra)
    time.sleep(0.2)

Expected: the first call or two succeed and x-ratelimit-remaining-tokens drops fast (the uncapped reservation is large against 1,000 TPM); within a few iterations you get 429 with a Retry-After. Note which remaining header was near zero — it will be remaining-tokens (a TPM hit), because the uncapped reservation dominates.

Step 5 — Fix it: add a max_tokens cap and honour Retry-After. Re-run with max_tokens=64 and backoff, and watch far more calls succeed before any throttle:

import os, time, random
from openai import AzureOpenAI, RateLimitError

client = AzureOpenAI(api_version="2024-10-21",
                     azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
                     api_key=os.environ["AZURE_OPENAI_KEY"])

def call(i, attempts=4):
    for a in range(attempts):
        try:
            return client.chat.completions.create(
                model="gpt-4o-mini", max_tokens=64,         # tiny reservation
                messages=[{"role": "user", "content": "One sentence about Azure."}])
        except RateLimitError as e:
            ra = e.response.headers.get("retry-after") if e.response else None
            wait = (float(ra) if ra else min(2 ** a, 30)) + random.uniform(0, 0.5)
            print(i, f"429 -> sleeping {wait:.1f}s")
            if a == attempts - 1: raise
            time.sleep(wait)

for i in range(12):
    call(i); print(i, "OK")

Expected: with the small max_tokens reservation, many more calls fit before a 429; the few that throttle now wait the server-prescribed time and succeed, instead of failing the user.

Step 6 — Teardown. Delete the deployment (and the whole RG if it was lab-only):

az cognitiveservices account deployment delete -n $ACC -g $RG --deployment-name gpt-4o-mini
az group delete -n $RG --yes --no-wait

The lab makes the two lessons physical: uncapped max_tokens exhausts TPM far below real usage, and honouring Retry-After turns a hard failure into a brief, recoverable wait.

Common mistakes & troubleshooting

This is the playbook — the centrepiece. Each row is a real 429 failure mode: symptom, root cause, the exact way to confirm it, and the fix. It spans basic mistakes (no max_tokens, bad retries) and advanced ones (subscription-quota exhaustion, PTU saturation, multi-tenant starvation).

# Symptom Root cause Confirm (exact command / portal path) Fix
1 429s under any real load; remaining-tokens hits 0 first TPM exceeded — large prompts/completions Log x-ratelimit-remaining-tokens; near 0 at the 429 Cap max_tokens, trim prompt/RAG, raise TPM, cache
2 429s from many tiny calls; remaining-requests hits 0 first RPM exceeded — call volume, not size Log x-ratelimit-remaining-requests; near 0 at the 429 Batch, add concurrency limiter, raise TPM (RPM is derived)
3 429s never clear; error rate keeps climbing Retry storm — immediate/no-backoff retries Compare app retry cadence to Retry-After in logs Honour Retry-After; exponential backoff + jitter; bound attempts
4 429s far below expected usage; few calls “fit” No max_tokens → large default reservation per call Inspect request body; max_tokens absent Set a tight max_tokens (smallest you tolerate)
5 Can’t raise TPM; “quota exceeded” on update/create Regional/model quota exhausted Quotas blade (assigned ≈ limit); az cognitiveservices usage list Request a quota increase; use another region or Global
6 Brand-new deployment 429s immediately at modest load Default quota too low for launch traffic Quotas blade shows tiny default TPM Request increase early (not instant); raise --sku-capacity
7 One tenant’s traffic 429s everyone Multi-tenant starvation — no per-caller throttle Correlate 429 spikes to one caller/key in logs Per-tenant rate limit (APIM); separate deployments/keys
8 Constant 429s at all hours, not bursty Sustained demand > shared-pool slice Token-usage metric pinned at ceiling 24/7 Move to PTU (or Global Standard for more headroom)
9 PTU deployment returns 429 under peak PTU saturation — provisioned units undersized azure-openai-ptu-utilization metric at ~100% Add PTUs, or configure spillover to Standard
10 429s only during spikes; baseline fine Burst exceeds regional slice Token metric spikes to ceiling only at peaks Global Standard (bigger pool); or PTU + spillover
11 Bulk/embeddings job 429s and starves the live app Batch work on the live deployment Job runs against the interactive deployment Move to the Batch API (separate quota, ~50% cheaper)
12 429s after enabling streaming + retries Retried streaming calls resend the whole prompt Token usage spikes per retried stream Bound stream retries; non-streaming for retry-heavy paths
13 429s with remaining headers absent / immediate repeats Deployment still provisioning, or wrong deployment name az ... deployment show (state); verify name = model arg Wait for ready state; use the deployment name, not base model id
14 Embeddings calls 429 while chat is fine Embeddings deployment has its own (small) quota Check the embeddings deployment’s assigned TPM separately Raise the embeddings deployment TPM; batch embeddings
15 Intermittent 429s right after a quota bump New capacity not picked up / change still propagating az ... deployment show capacity vs what you set Confirm capacity applied; allow brief propagation
16 429 count high in metrics but users unaffected Backoff is working — transient throttles absorbed 429 metric > 0 but user error rate ~0 No action; this is healthy. Alert on user errors, not raw 429

The error-code reference

429 is the headline, but adjacent codes confuse triage. Know what is and is not a rate-limit problem:

Code Meaning on Azure OpenAI Likely cause How to confirm Is it a 429/quota issue?
429 Too Many Requests Rate/quota exceeded (TPM or RPM) Over your assigned limit; or retry storm Retry-After + remaining-* headers Yes — the whole article
429 (insufficient_quota) No quota to even admit Regional/subscription quota exhausted Quotas blade; usage list Yes — but it’s capacity, not rate
400 content too long Prompt + completion exceed context window Oversized prompt for the model Error message names the token limit No — shrink prompt / raise model
400 content filter Response blocked by content safety Prompt/response hit a filter finish_reason: content_filter No — adjust content or RAI policy
401 / 403 Auth failed / forbidden Bad key, wrong endpoint, RBAC/network rule Verify key/endpoint; check access No — auth/network, see Key Vault/MI
404 deployment not found Wrong deployment name or not ready Used base model id, or still provisioning az ... deployment show No — fix the name / wait
408 / 504 timeout Slow upstream or client timeout Very large generation; network Compare duration to client timeout No — usually latency, not quota
500 / 503 Transient platform error Service-side blip Retry with backoff; check status No — but retry like a 429 (backoff)

Three reading notes that save the most time:

Distinction The trap How to tell them apart
429 (rate) vs 429 (no quota) Both say 429, opposite fixes Rate carries a short Retry-After and recovers; no-quota persists until you raise quota (Quotas blade: assigned ≈ limit)
429 vs 400 (context length) “Too big” feels like a limit 400 names a token/context limit and never has Retry-After; a per-request size issue, not a rate one
High 429 metric vs real impact Alerting on raw 429s pages you needlessly With correct backoff, transient 429s are absorbed; alert on user-facing error rate, not raw 429 count

Per-symptom detail on the three you will hit most

TPM exceeded (rows 1, 4). Signature: x-ratelimit-remaining-tokens trending to zero while remaining-requests stays healthy. Amplifiers: fat prompts (long system prompts, too many RAG chunks) and a missing max_tokens. Confirm by logging both remaining headers; fix by capping max_tokens and trimming context first, then raise TPM only if demand still exceeds the slice.

RPM exceeded (row 2). Signature: remaining-requests hits zero while remaining-tokens is fine — classic for batch loops and chatty agents firing many small calls. You can buy more RPM by raising TPM (RPM is derived), but the cheaper fix is usually to batch, add a concurrency limiter, or move bulk work to the Batch API.

Retry storm (row 3). Signature: a 429 rate that climbs instead of recovering, with logs showing retries firing faster than Retry-After asks. Self-inflicted — every immediate retry is another request against the exhausted limit. Confirm by overlaying retry timestamps on Retry-After; fix by honouring Retry-After, backoff with jitter, and bounded attempts.

Best practices

Security notes

Cost & sizing

The bill on Standard/Global is pay-per-token — charged for prompt + completion tokens at the model’s per-1K rate, with no charge for the TPM ceiling (TPM is a rate limit, not a reservation). So the 429 fixes that cut tokens — max_tokens, prompt trimming, caching, smaller models — also cut cost; efficiency and reliability pull the same direction.

Lever What drives the cost How to right-size Rough figure (illustrative)
Tokens processed (Standard/Global) Prompt + completion tokens × model rate Cap max_tokens; trim context; cache; smaller model gpt-4o-mini is a fraction of gpt-4o per 1K tokens
Model choice Flagship vs mini per-token rate Use mini wherever quality allows Often 10×+ cheaper for routine tasks
Batch API Same tokens, async Move bulk work here ~50% of the synchronous per-token price
PTU (provisioned) Reserved units, billed hourly Size to steady base, not peak; reserve monthly/annually Hourly PTU is pricey; reservations cut it sharply
Cache hit rate Calls avoided entirely Cache deterministic/repeated prompts Each hit = ₹0 tokens, ₹0 RPM
Quota (TPM) Free — it’s a rate limit, not a reservation Assign enough for peak; raising it costs nothing per se You pay for tokens used, not TPM assigned

Sizing rules of thumb: for bursty or variable workloads, stay pay-per-token (Standard/Global) and assign generous TPM (it is free) for burst headroom. For steady, high-volume, latency-sensitive workloads, model PTUs at a monthly/annual reservation against your token spend; above a sustained utilisation threshold, reserved PTU is both cheaper and more reliable. Pair capacity planning with demand reduction first — there is no free-tier for Azure OpenAI, so every wasted token is real money, and a tight max_tokens plus a cache often saves more than any quota change. For account-wide guardrails, Azure Cost Management: budgets & alerts in the first 30 days sets up the alerts.

Interview & exam questions

1. What does a 429 from Azure OpenAI mean, and what are the two limits behind it? A 429 (Too Many Requests) means a valid request exceeded your assigned rate. Azure OpenAI enforces two simultaneous limits on Standard-family deployments: TPM (tokens/minute, prompt + completion) and RPM (requests/minute). Exceeding either returns a 429. (Maps to AI-102.)

2. How is RPM determined for a deployment? You do not set RPM directly — you assign TPM, and RPM is derived at 6 RPM per 1,000 TPM (so 60,000 TPM yields 360 RPM). Raising TPM proportionally raises RPM, which is why a request-rate problem can sometimes be solved by adding token quota.

3. Why does omitting max_tokens cause more 429s? The platform estimates a call’s token cost before running the model, reserving prompt tokens plus a completion budget equal to max_tokens (or a large default if omitted). Without max_tokens it reserves the large default per call, so fewer calls fit in your TPM window and you 429 far below real usage. Capping max_tokens lets more genuine traffic through.

4. How do you tell whether a 429 was a TPM or an RPM hit? Read the response headers: if x-ratelimit-remaining-tokens was trending to zero, it was TPM; if x-ratelimit-remaining-requests was, it was RPM. They have opposite fixes, so this single observation routes the whole diagnosis.

5. What is the correct way to retry a 429, and why? Honour the Retry-After header (wait at least that long), add exponential backoff with jitter, and bound the attempts. Immediate or fixed-interval retries fire fresh requests against the exhausted limit and synchronise clients into a thundering herd, amplifying the storm instead of draining the queue.

6. When would you choose Global Standard over regional Standard? When you need burst absorption or higher throughput and can tolerate processing outside your resource’s region. Global Standard draws from a much larger shared pool (often cheaper per token); regional Standard keeps processing in-region for data-residency but has a smaller pool that 429s earlier under burst.

7. What problem does Provisioned Throughput (PTU) solve, and what’s the trade-off? PTU gives reserved capacity with guaranteed throughput and predictable latency — no shared-pool 429s, only saturating your own units. The trade-off is cost: you pay for reserved capacity whether or not you use it, so it pays off only at steady high utilisation. Spillover can overflow bursts to a Standard deployment.

8. Your deployment is at its assigned TPM but you still can’t raise it. Why? The regional/subscription quota for that model family is likely exhausted across all your deployments — assigned ≈ limit in the Quotas blade. You need a quota-increase request (not instant), a different region, or a Global deployment drawing from a larger pool.

9. How does the Batch API help with 429s? The Batch API runs requests asynchronously (up to 24-hour turnaround) against a separate quota at roughly half the per-token price. Moving bulk work (embeddings, enrichment) there keeps it off your live deployment’s TPM, so interactive traffic stops being starved by background jobs.

10. Why might you alert on user-facing error rate instead of raw 429 count? With correct backoff, transient 429s are absorbed automatically — a non-zero 429 metric is often healthy. Paging on every 429 generates noise and trains the team to ignore alerts; alerting on user-facing errors (requests that failed after retries) surfaces only real impact.

11. How do you reduce token consumption without losing functionality? Cap max_tokens, trim prompts and RAG context (fewer/shorter chunks), cache deterministic or repeated responses, and use a smaller model (gpt-4o-mini) where quality allows. Each cuts both 429 risk and cost, since the bill is per token.

12. What’s the difference between a 429 and a 400 “context length” error? A 429 is a rate problem — you exceeded TPM/RPM and should back off and retry (it carries Retry-After). A 400 context-length error is a size problem with one request — prompt plus completion exceeds the model’s context window — and retrying unchanged will fail again; shrink the prompt or use a larger-context model.

Quick check

  1. A deployment is assigned 50,000 TPM. What is its derived RPM?
  2. You get a 429 and x-ratelimit-remaining-requests was at 0 while remaining-tokens was healthy. Which limit did you hit, and name one fix.
  3. Why does omitting max_tokens make 429s more likely even if your real completions are short?
  4. Two requests both return 429 with Retry-After: 12. What is the correct retry behaviour?
  5. Your workload is steady, high-volume and latency-sensitive, and you 429 constantly. Which deployment type fits, and what’s the cost trade-off?

Answers

  1. 300 RPM (6 RPM per 1,000 TPM × 50 = 300).
  2. RPM — too many requests. Fix: batch the calls, add a concurrency limiter, or raise TPM (RPM is derived from it).
  3. The platform reserves a completion budget equal to max_tokens (or a large default if omitted) before running the model; the big default reservation fills your TPM window, so fewer calls fit and you 429 below real usage.
  4. Honour Retry-After: wait at least 12 seconds (plus a little jitter), then retry; on repeat, back off exponentially and stop after a bounded number of attempts — never retry immediately or in lockstep.
  5. Provisioned Throughput (PTU) for guaranteed throughput and predictable latency; the trade-off is you pay for reserved capacity whether or not it’s used, so it only pays off at steady high utilisation (a monthly/annual reservation lowers the rate).

Glossary

Next steps

AzureAzure OpenAITroubleshooting429Rate LimitingTPMQuotaAI/ML
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading