429 rate limit errors after scaling GPT-4o microservices in Kubernetes

429 Rate‑Limit Errors After Scaling GPT‑4o Microservices in Kubernetes

Problem

After deploying additional replicas of several microservices that call the OpenAI gpt‑4o endpoint, the production cluster began returning HTTP 429 Rate limit exceeded responses during traffic spikes. The failures are intermittent, cause request timeouts in downstream services, and trigger cascading latency spikes.

Typical log excerpt (service A)

2026-10-20T14:32:07.842Z ERROR request_id=9f2c3d7b-1a4e-4b9e-8f6a-3d5c7e9b2a1c msg="OpenAI API call failed" status=429 body="{\"error\":{\"message\":\"Rate limit exceeded\",\"type\":\"rate_limit_error\"}}"

Impact

  • Increased end‑to‑end latency for user‑facing APIs (average 2.3 s → 7.8 s).
  • Error‑rate spikes in Prometheus alerts (HTTP 5xx > 5 %).
  • Reduced throughput as request queues back up in the ingress controller.

Root Cause

The OpenAI platform enforces a per‑account request‑per‑minute quota and a concurrent request limit. Scaling the microservices increased the aggregate request rate beyond the configured quota. Because each service makes a synchronous call to https://api.openai.com/v1/chat/completions, the burst traffic during peak load caused the API gateway to reject excess requests with 429.

Additional contributing factors:

  • Absence of client‑side backoff: The original HTTP client performed a single request and failed fast.
  • Uniform retry policy across services: All services retried immediately, amplifying the burst.
  • Static Kubernetes resources: No rate‑limiting sidecar or token bucket was applied before reaching OpenAI.

Debug & Investigation

  1. Confirm 429 source. Use kubectl logs and filter for status 429.
    kubectl logs -l app=service-a -c app-container --since=5m | grep 'status=429'
  2. Correlate request volume. Query Prometheus for http_requests_total{service="service-a",code="429"} and compare against rate_limit_quota from OpenAI account dashboard.
  3. Inspect OpenAI response headers. The API returns Retry-After and X-RateLimit-Remaining.
    curl -i -H "Authorization: Bearer $OPENAI_KEY" https://api.openai.com/v1/chat/completions \
      -d '{"model":"gpt-4o","messages":[{"role":"user","content":"Hello"}]}' -s -D -

    Sample headers on 429:

    HTTP/1.1 429 Too Many Requests
    Content-Type: application/json
    Retry-After: 12
    X-RateLimit-Limit: 3500
    X-RateLimit-Remaining: 0
    X-RateLimit-Reset: 1697827200
  4. Check pod concurrency. Verify the number of concurrent requests per pod using ss -anp | grep ESTAB inside a pod.
  5. Validate Kubernetes autoscaling. Confirm that the Horizontal Pod Autoscaler (HPA) scaled as expected and did not overshoot the quota.
    kubectl get hpa -n prod

Solution

Implement a resilient retry‑with‑exponential‑backoff strategy that respects the Retry-After header, and introduce a client‑side token bucket to smooth bursts.

Before: Direct request without retry

import requests, os

def call_gpt4o(prompt: str) -> str:
    resp = requests.post(
        "https://api.openai.com/v1/chat/completions",
        headers={"Authorization": f"Bearer {os.getenv('OPENAI_KEY')}"},
        json={"model": "gpt-4o", "messages": [{"role": "user", "content": prompt}]},
        timeout=10,
    )
    resp.raise_for_status()
    return resp.json()["choices"][0]["message"]["content"]

After: Retry with backoff and token bucket

import os, time, threading
import requests
from collections import deque

# Simple in‑process token bucket (capacity = 3000 req/min)
class TokenBucket:
    def __init__(self, rate_per_minute, capacity):
        self.rate = rate_per_minute / 60.0
        self.capacity = capacity
        self.tokens = capacity
        self.lock = threading.Lock()
        self.last_refill = time.monotonic()

    def _refill(self):
        now = time.monotonic()
        elapsed = now - self.last_refill
        added = elapsed * self.rate
        if added > 0:
            self.tokens = min(self.capacity, self.tokens + added)
            self.last_refill = now

    def acquire(self):
        while True:
            with self.lock:
                self._refill()
                if self.tokens >= 1:
                    self.tokens -= 1
                    return
            time.sleep(0.01)  # wait 10 ms before retrying

bucket = TokenBucket(rate_per_minute=3000, capacity=3000)

def call_gpt4o(prompt: str, max_retries: int = 5) -> str:
    bucket.acquire()  # throttle before hitting the API
    url = "https://api.openai.com/v1/chat/completions"
    headers = {"Authorization": f"Bearer {os.getenv('OPENAI_KEY')}"}
    payload = {"model": "gpt-4o", "messages": [{"role": "user", "content": prompt}]}
    backoff = 1  # seconds

    for attempt in range(max_retries + 1):
        try:
            resp = requests.post(url, headers=headers, json=payload, timeout=10)
            if resp.status_code == 429:
                retry_after = int(resp.headers.get("Retry-After", backoff))
                time.sleep(retry_after)
                raise requests.exceptions.RetryError("Rate limited")
            resp.raise_for_status()
            return resp.json()["choices"][0]["message"]["content"]
        except (requests.exceptions.RequestException, requests.exceptions.RetryError) as e:
            if attempt == max_retries:
                raise
            time.sleep(backoff)
            backoff = min(backoff * 2, 30)  # exponential backoff, cap at 30 s

Key points:

  • The token bucket enforces a steady request rate (3000 req/min in this example) regardless of pod count.
  • On a 429 response the client respects Retry-After and then applies exponential backoff.
  • Maximum retry count prevents infinite loops.

Kubernetes Deployment Changes

Update the container image and add a ConfigMap to make the rate‑limit parameters configurable without rebuilding.

# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: gpt4o-rate-limit
  namespace: prod
data:
  RATE_PER_MINUTE: "3000"
  BUCKET_CAPACITY: "3000"
  MAX_RETRIES: "5"
# deployment.yaml (excerpt)
...
        envFrom:
        - configMapRef:
            name: gpt4o-rate-limit
        env:
        - name: OPENAI_KEY
          valueFrom:
            secretKeyRef:
              name: openai-credentials
              key: api-key
...

Verify

  1. Smoke test the endpoint.
    kubectl exec -it $(kubectl get pod -l app=service-a -o jsonpath='{.items[0].metadata.name}') -- curl -s http://localhost:8080/healthz
  2. Monitor 429 metric. In Prometheus:
    rate(http_requests_total{code="429"}[1m]) < 0.1
  3. Check token bucket utilization. Add a debug endpoint that returns bucket.tokens and verify it stays above zero during load tests.
  4. Load test with hey or locust at peak traffic levels. Ensure latency stays within SLA and no new 429 spikes appear.

Prevent

  • Capacity planning. Align the token bucket rate with the OpenAI account’s documented limits (including any per‑minute or per‑second caps).
  • Centralized rate‑limit service. Deploy a sidecar (e.g., Envoy with a rate‑limit filter) that enforces the same token bucket across all services, guaranteeing uniform throttling.
  • Observability. Export bucket.tokens and retry_attempts as Prometheus gauges; set alerts when the bucket empties or retry count exceeds a threshold.
  • Graceful degradation. When the bucket is exhausted, return a cached or fallback response instead of queuing more retries.
  • OpenAI quota alerts. Use the OpenAI usage dashboard API to trigger alerts before hitting the hard limit.

FAQ

  1. Why does the 429 error appear only after scaling pods?
    Because each replica adds its own request stream. The aggregate rate exceeds the account’s quota, while a single replica stays within limits.
  2. Should I rely solely on the Retry-After header?
    It is a required part of the backoff strategy, but combining it with exponential backoff and a client‑side token bucket prevents immediate retry storms.
  3. Can I increase the OpenAI quota instead of throttling?
    You can request a higher quota, but it is still advisable to implement client‑side throttling to protect against unexpected spikes and to stay within any undisclosed internal limits.
  4. Is a sidecar proxy preferable to in‑process throttling?
    A sidecar provides a single point of control, works for services written in any language, and can be managed via Istio or Envoy Rate Limit Service. In‑process throttling is simpler for a single language stack but duplicates logic across services.
  5. What metrics should I monitor to catch future rate‑limit issues early?
    Track http_requests_total{code="429"}, token bucket fill level, retry attempt counters, and OpenAI usage metrics (requests/minute, tokens consumed).

Related Topic Hub: LLM Systems Troubleshooting Hub