Azure Azure OpenAI rate limiting FinOps LLM

Azure OpenAI Service - 429 Rate Limit When Token or Request Quota Is Exceeded

Resolving 429 errors in Azure OpenAI: identifying the bottleneck (TPM vs RPM), implementing retry logic, increasing quota, or switching to PTU.

Jerzy Kopaczewski ·
Azure OpenAI API returns HTTP 429 Too Many Requests when your application exceeds its allocated token quota (TPM) or the number of requests per minute (RPM). The result: users experience errors in chatbots, RAG pipelines, and automated document processing systems. During peak hours this can mean losing tens of percent of production traffic to GPT-4o or GPT-4 models.

This runbook covers diagnosing and resolving rate limiting in Azure OpenAI Service. For a detailed analysis of AI model costs and cloud architecture, see our article Azure OpenAI vs self-hosted models - cost, latency, and data residency in the EU. If you need help optimising costs and performance of AI workloads - book a consultation.

Symptom

The application receives an HTTP 429 response from Azure OpenAI API. Logs show a RateLimitError or 429 Too Many Requests message:

# Sprawdź aktualne zużycie quota dla deploymentu
az cognitiveservices usage list \
  --name my-openai-resource \
  --resource-group rg-ai \
  --query "[?name.value=='OpenAI.Standard.gpt-4o'].{model:name.value, current:currentValue, limit:limit}" -o table

# Sprawdź metryki rate limitingu z ostatnich 30 minut
az monitor metrics list \
  --resource /subscriptions/<sub-id>/resourceGroups/rg-ai/providers/Microsoft.CognitiveServices/accounts/my-openai-resource \
  --metric "AzureOpenAIRequests" \
  --dimension StatusCode \
  --interval PT1M \
  --start-time $(date -u -d '-30 minutes' +%Y-%m-%dT%H:%M:%SZ) \
  --query "value[0].timeseries[?metricValues[?statusCode=='429']]" -o json

# Lista deploymentów z ich limitami
az cognitiveservices account deployment list \
  --name my-openai-resource \
  --resource-group rg-ai \
  --query "[].{model:properties.model.name, version:properties.model.version, capacity:sku.capacity, name:name}" -o table

In the HTTP response from Azure OpenAI, check the rate limit headers:

# Pojedyncze zapytanie testowe z wyświetleniem nagłówków
curl -s -D - https://my-openai-resource.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-10-21 \
  -H "Content-Type: application/json" \
  -H "api-key: $AZURE_OPENAI_KEY" \
  -d '{"messages":[{"role":"user","content":"test"}],"max_tokens":5}' \
  2>&1 | grep -i "x-ratelimit\|retry-after"

# Spodziewane nagłówki:
# x-ratelimit-remaining-tokens: 0        <-- brak dostępnych tokenów
# x-ratelimit-remaining-requests: 0      <-- brak dostępnych zapytań
# retry-after: 12                         <-- czas oczekiwania w sekundach

If x-ratelimit-remaining-tokens or x-ratelimit-remaining-requests is 0 - the deployment has exhausted its quota for the current minute.

The “high demand / peak load” variant (shared-capacity throttle)

A second, often-confused 429 looks like this:

The system is currently experiencing high demand and cannot process your request.
Your request exceeds the maximum usage size allowed during peak load.
Please retry after 6 seconds. For improved capacity reliability, consider switching
to Provisioned Throughput.

This is not your own TPM/RPM quota being exhausted - it is the shared Standard (pay-as-you-go) capacity pool being saturated across all tenants in the region during a demand spike. The tell is the wording “high demand” and “peak load” plus a retry-after even when your own x-ratelimit-remaining-tokens is not zero. It matters because the fix is different: raising your quota will not help (the pool, not your limit, is the bottleneck). The two levers that do help are (1) retry with backoff on the retry-after value, and (2) Provisioned Throughput (PTU), which reserves dedicated capacity outside the shared pool - exactly what the error message suggests. Data Zone / regional-residency deployments see this more often because their shared pools are smaller. Everything in the Resolution section below applies; the PTU step (D) is the durable fix for this variant specifically.

Root Cause

Azure OpenAI applies two independent limits, either of which can trigger a 429 error:

  • TPM (Tokens Per Minute) - the total number of tokens (prompt + completion) processed within a minute. For a GPT-4o deployment, the default quota in the Sweden Central region is e.g. 80K TPM. Long prompts (RAG with document context) exhaust this limit quickly.

  • RPM (Requests Per Minute) - the number of API calls per minute. The RPM value is automatically calculated as 6x the TPM value / 1000. For 80K TPM = 480 RPM. Many short requests (e.g. sentence classification) can hit RPM before exhausting TPM.

Additional factors contributing to the problem:

  • Global Standard vs Data Zone deployments - deployment types have different base quotas. Global Standard offers higher default limits, but Data Zone (with regional data residency guarantees) has lower pools, particularly for GPT-4o in European regions. Quota is shared across all deployments of the same model within a subscription and region.

  • Traffic burst - even if average usage fits within the limit, a sudden traffic spike (e.g. a cron job processing a batch of documents) can exhaust the quota in the first seconds of a minute. Azure does not buffer the overflow - it immediately returns 429.

  • Token estimation mismatch - the application does not account for tokens from the system prompt, function calling schema, or conversation history. Actual TPM consumption is 2-3x higher than estimated from user messages alone.

Resolution

A) Retry with exponential backoff:

The simplest solution for sporadic 429s. Implementation using the tenacity library:

import openai
from tenacity import retry, wait_exponential, stop_after_attempt, retry_if_exception_type
from openai import AzureOpenAI, RateLimitError

client = AzureOpenAI(
    azure_endpoint="https://my-openai-resource.openai.azure.com",
    api_version="2024-10-21",
    api_key=os.environ["AZURE_OPENAI_KEY"],
)

@retry(
    retry=retry_if_exception_type(RateLimitError),
    wait=wait_exponential(multiplier=1, min=2, max=60),
    stop=stop_after_attempt(6),
    before_sleep=lambda retry_state: print(
        f"429 received, retry {retry_state.attempt_number}/6, "
        f"waiting {retry_state.next_action.sleep}s..."
    ),
)
def call_openai(messages: list, model: str = "gpt-4o") -> str:
    response = client.chat.completions.create(
        model=model,
        messages=messages,
        max_tokens=1024,
    )
    return response.choices[0].message.content


# Alternatywnie - wykorzystaj nagłówek retry-after z odpowiedzi
@retry(
    retry=retry_if_exception_type(RateLimitError),
    wait=lambda retry_state: float(
        getattr(retry_state.outcome.exception(), 'response', None)
        and retry_state.outcome.exception().response.headers.get('retry-after', 5)
        or 5
    ),
    stop=stop_after_attempt(5),
)
def call_openai_with_retry_after(messages: list) -> str:
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
    )
    return response.choices[0].message.content

B) Increase quota via Azure Portal / CLI:

# Sprawdź aktualne limity quota w regionie
az cognitiveservices model list \
  --location swedencentral \
  --query "[?model.name=='gpt-4o'].{model:model.name, version:model.version, maxCapacity:model.skus[0].capacity.maximum}" -o table

# Sprawdź aktualną capacity deploymentu
az cognitiveservices account deployment show \
  --name my-openai-resource \
  --resource-group rg-ai \
  --deployment-name gpt-4o \
  --query "{capacity:sku.capacity, model:properties.model.name}" -o json

# Zwiększ capacity deploymentu (np. z 80K do 150K TPM)
az cognitiveservices account deployment create \
  --name my-openai-resource \
  --resource-group rg-ai \
  --deployment-name gpt-4o \
  --model-name gpt-4o \
  --model-version "2024-08-06" \
  --model-format OpenAI \
  --sku-capacity 150 \
  --sku-name "GlobalStandard"

# Jeśli region nie ma wystarczającej capacity - sprawdź dostępność w innych regionach
az cognitiveservices model list \
  --location eastus2 \
  --query "[?model.name=='gpt-4o'].{version:model.version, available:model.skus[0].capacity.maximum}" -o table

Note: The sku-capacity value is specified in thousands of TPM. A value of 150 means 150,000 TPM.

C) Load balancing across multiple deployments (LiteLLM):

Distribute traffic across several OpenAI resources in different regions:

# litellm_config.yaml
model_list:
  - model_name: gpt-4o
    litellm_params:
      model: azure/gpt-4o
      api_base: https://openai-swedencentral.openai.azure.com/
      api_key: os.environ/AZURE_OPENAI_KEY_SWEDEN
      api_version: "2024-10-21"
    model_info:
      tpm: 80000
      rpm: 480

  - model_name: gpt-4o
    litellm_params:
      model: azure/gpt-4o
      api_base: https://openai-eastus2.openai.azure.com/
      api_key: os.environ/AZURE_OPENAI_KEY_EASTUS2
      api_version: "2024-10-21"
    model_info:
      tpm: 80000
      rpm: 480

  - model_name: gpt-4o
    litellm_params:
      model: azure/gpt-4o
      api_base: https://openai-francecentral.openai.azure.com/
      api_key: os.environ/AZURE_OPENAI_KEY_FRANCE
      api_version: "2024-10-21"
    model_info:
      tpm: 60000
      rpm: 360

router_settings:
  routing_strategy: "usage-based-routing-v2"
  enable_pre_call_checks: true    # sprawdza TPM/RPM przed wysłaniem
  retry_after: 5
  num_retries: 3
  allowed_fails: 2
  cooldown_time: 30               # sekundy cooldown po 429
# Uruchom LiteLLM proxy
litellm --config litellm_config.yaml --port 4000

# Test - zapytania automatycznie routowane do dostępnego deploymentu
curl http://localhost:4000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-4o","messages":[{"role":"user","content":"test"}]}'

D) Switch to Provisioned Throughput Units (PTU):

For stable, predictable production workloads - PTU guarantees consistent throughput without throttling:

# Sprawdź dostępność PTU w regionie
az cognitiveservices model list \
  --location swedencentral \
  --query "[?model.name=='gpt-4o'].model.skus[?name=='ProvisionedManaged'].{name:name, minCapacity:capacity.minimum, maxCapacity:capacity.maximum}" -o json

# Utwórz deployment typu Provisioned (minimalnie 50 PTU)
az cognitiveservices account deployment create \
  --name my-openai-resource \
  --resource-group rg-ai \
  --deployment-name gpt-4o-ptu \
  --model-name gpt-4o \
  --model-version "2024-08-06" \
  --model-format OpenAI \
  --sku-capacity 50 \
  --sku-name "ProvisionedManaged"

# Monitoruj utilization PTU (powinno być <80% dla rezerwy)
az monitor metrics list \
  --resource /subscriptions/<sub-id>/resourceGroups/rg-ai/providers/Microsoft.CognitiveServices/accounts/my-openai-resource \
  --metric "ProvisionedManagedUtilizationV2" \
  --dimension ModelDeploymentName \
  --interval PT1M \
  --start-time $(date -u -d '-60 minutes' +%Y-%m-%dT%H:%M:%SZ) \
  --query "value[0].timeseries[].data[-5:]" -o table

Cost note: PTU is a reservation commitment (billed hourly/monthly). 50 PTU for GPT-4o costs approximately $2/hour. It pays off when pay-as-you-go usage exceeds roughly $1.5/hour on a given deployment. Details in the calculator: Azure OpenAI Pricing.

E) Automatic Standard → PTU failover (Python client):

Combines cheap Standard deployment with guaranteed PTU as fallback:

# azure_openai_client.py - failover from Standard to PTU
from openai import AzureOpenAI

class AzureOpenAIWithFallback:
    """Azure OpenAI client with automatic failover to PTU on 429."""

    def __init__(self):
        self.primary = AzureOpenAI(
            azure_endpoint="https://my-openai-resource.openai.azure.com/",
            api_key="...",
            api_version="2024-10-21",
        )
        self.primary_deployment = "gpt-4o"  # Standard

        self.fallback = AzureOpenAI(
            azure_endpoint="https://my-openai-resource.openai.azure.com/",
            api_key="...",
            api_version="2024-10-21",
        )
        self.fallback_deployment = "gpt-4o-ptu"  # ProvisionedManaged

    def chat_completion(self, messages, **kwargs):
        """Try Standard first, switch to PTU on 429."""
        try:
            return self.primary.chat.completions.create(
                model=self.primary_deployment,
                messages=messages,
                **kwargs,
            )
        except Exception as e:
            if "429" in str(e) or "rate limit" in str(e).lower():
                return self.fallback.chat.completions.create(
                    model=self.fallback_deployment,
                    messages=messages,
                    **kwargs,
                )
            raise

F) Multi-region failover via Azure API Management:

Routing with automatic region switch on 429:

<!-- APIM policy - cross-region routing with failover -->
<policies>
  <inbound>
    <base />
    <set-backend-service base-url="https://openai-swedencentral.openai.azure.com/" />
  </inbound>
  <backend>
    <retry condition="@(context.Response.StatusCode == 429)"
           count="1" interval="0" first-fast-retry="true">
      <set-backend-service base-url="https://openai-eastus2.openai.azure.com/" />
      <forward-request />
    </retry>
  </backend>
  <outbound>
    <base />
    <set-header name="x-region-used" exists-action="override">
      <value>@(context.Response.Headers.GetValueOrDefault("x-ms-region","unknown"))</value>
    </set-header>
  </outbound>
</policies>

Validation

# 1. Wyślij serię zapytań testowych i sprawdź brak 429
for i in $(seq 1 20); do
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
    https://my-openai-resource.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-10-21 \
    -H "Content-Type: application/json" \
    -H "api-key: $AZURE_OPENAI_KEY" \
    -d '{"messages":[{"role":"user","content":"Say OK"}],"max_tokens":5}')
  echo "Request $i: HTTP $STATUS"
  sleep 0.5
done
# Expected: wszystkie odpowiedzi HTTP 200

# 2. Sprawdź metryki - brak nowych 429 w ostatnich 5 minutach
az monitor metrics list \
  --resource /subscriptions/<sub-id>/resourceGroups/rg-ai/providers/Microsoft.CognitiveServices/accounts/my-openai-resource \
  --metric "AzureOpenAIRequests" \
  --dimension StatusCode \
  --interval PT1M \
  --start-time $(date -u -d '-5 minutes' +%Y-%m-%dT%H:%M:%SZ) \
  --query "value[0].timeseries[].data[].{time:timeStamp, total:total}" -o table
# Expected: brak wierszy z StatusCode=429

# 3. Skonfiguruj alert na przyszłość
az monitor metrics alert create \
  --name "alert-openai-429-rate-limit" \
  --resource-group rg-ai \
  --scopes /subscriptions/<sub-id>/resourceGroups/rg-ai/providers/Microsoft.CognitiveServices/accounts/my-openai-resource \
  --condition "total AzureOpenAIRequests where StatusCode includes 429 > 10" \
  --window-size 5m \
  --evaluation-frequency 1m \
  --severity 2 \
  --action-group /subscriptions/<sub-id>/resourceGroups/rg-monitoring/providers/Microsoft.Insights/actionGroups/ag-platform-team \
  --description "Azure OpenAI rate limiting - >10 requests z 429 w 5 minut"

Once the fix is deployed, verify in Application Insights that the OpenAI call success rate has returned to >99%. Dashboard:

// Application Insights - KQL query do monitorowania 429
dependencies
| where timestamp > ago(1h)
| where target contains "openai.azure.com"
| summarize
    total=count(),
    failed_429=countif(resultCode == "429"),
    success_rate=round(100.0 * countif(resultCode == "200") / count(), 2)
  by bin(timestamp, 5m)
| order by timestamp desc

Frequently asked questions about Azure OpenAI 429 errors

What does "The system is currently experiencing high demand... maximum usage size allowed during peak load" mean?

This is an Azure OpenAI 429 from the shared Standard (pay-as-you-go) capacity pool, not your own quota. The full message - "The system is currently experiencing high demand and cannot process your request. Your request exceeds the maximum usage size allowed during peak load... For improved capacity reliability, consider switching to Provisioned Throughput" - means the region-wide pool is saturated across all tenants during a demand spike. The tell is the "high demand" / "peak load" wording plus a retry-after even when your own x-ratelimit-remaining-tokens is not zero. Raising your TPM/RPM quota will not help, because the bottleneck is the shared pool, not your limit.

Is the "peak load" 429 the same as a TPM/RPM quota 429?

No, and the distinction decides the fix. A TPM/RPM quota 429 is your own deployment exhausting its tokens-per-minute or requests-per-minute allocation - x-ratelimit-remaining-tokens hits 0 - and raising the quota solves it. The "high demand / peak load" 429 is the shared regional pool being saturated; your remaining-tokens header is often non-zero, and a quota increase changes nothing. You can also get it as "Rate limit exceeded: The system is currently experiencing high demand..." or, on streaming, "Stream disconnected before completion: The system is currently experiencing high demand..." - all three are the same shared-pool throttle.

How do I fix the "high demand / peak load" Azure OpenAI error?

Two levers work. First, retry with backoff honouring the retry-after value in the response, which clears transient spikes without piling on. Second, move the traffic that cannot absorb a throttle onto Provisioned Throughput (PTU) - dedicated capacity outside the shared pool, exactly what the error message suggests. Load balancing across deployments in multiple regions (for example with LiteLLM) spreads the risk further. A quota increase is the one thing that does not help here, because your own limit is not the constraint. Data Zone and regional-residency deployments hit this more often because their shared pools are smaller.

Why does retrying with a higher quota not stop the "peak load" 429?

Because the limit being hit is not yours. A quota increase raises your deployment's TPM/RPM ceiling, which only matters for the quota-exhaustion 429 where your remaining-tokens header is 0. The "high demand / peak load" variant is the region's shared Standard pool running out of capacity for everyone at once, so a bigger personal allocation still competes for a pool that has nothing left to give. The durable fix is to stop competing for the shared pool at all for critical traffic: reserve Provisioned Throughput, which carves out dedicated capacity that the demand spike cannot touch.
An unresolved 429 problem in Azure OpenAI directly impacts user experience: chatbots stop responding, RAG pipelines interrupt processing, and content generation automations halt. In architectures without retry logic, a single traffic spike can cause cascading failures across microservices that depend on the LLM. For production applications with an SLA above 99.9%, we recommend combining solutions B+C (increased quota + load balancing) or D (PTU) - retry alone (solution A) merely masks the problem without eliminating it.

 

Jerzy Kopaczewski

Azure OpenAI throttling your requests?

Book a free 30-minute call. We will analyse your traffic patterns, select the optimal quota strategy, and implement an architecture resilient to rate limiting.

Book a call