Skip to content
Ashish.
All posts
Diagram illustrating the three layers of LLMOps resilience: client-side retry, server-side queueing, and graceful degradation.
6 min readBackendBackend Developers, SREsFeatured#rate limiting#retries#backoff#degradation#429#queueing#llmops#reliability

Rate Limits, Retries, and Graceful Degradation

A practical guide to implementing rate limiting, exponential backoff, and graceful degradation in LLMOps to handle 429 errors and ensure system stability.

By Ashish KumarPart 5 of LLMOps in Production

In Large Language Model operations (LLMOps), the number 429 is not an error code for a broken resource; it is a flow-control signal. When an LLM provider returns 429 Too Many Requests, the model is healthy, the network is fine, and your code is correct. The system is simply asking you to slow down.

For backend developers and SREs, ignoring this signal leads to cascading failures. Sending rapid retries against a 429 response exacerbates the load, triggering deeper throttling or temporary IP bans. To build stable LLMOps systems, you must implement three distinct mechanisms: intelligent client-side retrying, asynchronous queueing, and graceful degradation.

This article is Part 5 of the LLMOps in Production series.

The Mechanism of 429

LLM providers enforce rate limits primarily through two metrics: Requests Per Minute (RPM) and Tokens Per Minute (TPM). These limits exist because inference is computationally expensive. Unlike traditional CRUD APIs where a database query might take milliseconds, an LLM generation can take seconds, consuming significant GPU memory and compute cycles.

When you exceed these limits, the provider’s gateway rejects your request. In HTTP terms, this is a 429. Crucially, the response often includes headers like Retry-After or x-ratelimit-reset, indicating when you can resume. However, relying solely on Retry-After is risky because it may be vague (e.g., "wait 60 seconds"). A stable system must implement its own backoff strategy.

Technical diagram showing RPM and TPM metrics on an LLM provider gateway. Illustrates how request volume and token count trigger 429 responses. Clean, architectural style, blue and gray palette.

Exponential Backoff with Jitter

The naive approach to handling 429 is to retry immediately or after a fixed delay. Both fail. Immediate retries create a "thundering herd" problem, where all clients hammer the server simultaneously once the limit resets. Fixed delays create synchronization, where all clients retry at the exact same moment, causing repeated 429s.

The solution is Exponential Backoff with Jitter.

The mechanism works as follows:

  1. Exponential Growth: Increase the wait time exponentially with each attempt (e.g., 1s, 2s, 4s, 8s). This ensures that if the provider is under heavy load, you wait longer.
  2. Jitter: Add a random component to the wait time. This desynchronizes clients. If 100 clients all hit a 429, they won’t all retry at second 4. They will retry between seconds 3 and 5.

Here is a practical Python implementation using the tenacity library, which is standard for this pattern:

import tenacity
import time
import random
 
def call_llm_with_retry(prompt, model="gpt-4"):
    @tenacity.retry(
        stop=tenacity.stop_after_attempt(5),
        wait=tenacity.wait_exponential(multiplier=1, min=4, max=10) + tenacity.wait_random(0, 1),
        retry=tenacity.retry_if_exception_type(Exception),
        before_sleep=lambda retry_state: print(f"Retrying in {retry_state.next_action.sleep:.2f}s")
    )
    def _call():
        response = openai.ChatCompletion.create(
            model=model,
            messages=[{"role": "user", "content": prompt}]
        )
        return response.choices[0].message.content
    
    try:
        return _call()
    except Exception as e:
        # If all retries fail, handle the final failure
        return "Service temporarily unavailable. Please try again later."

In this code, wait_exponential calculates the base wait time, and wait_random adds jitter. The max=10 cap prevents the wait time from becoming unreasonably long. Note that we catch the exception at the end. If the 5th attempt still fails, we do not throw an error to the user; we handle it gracefully.

Queueing as a Shock Absorber

Client-side retries improve stability, but they do not solve the problem of high concurrency. If 1,000 users hit your API simultaneously, your backend will still send 1,000 requests to the LLM provider, likely exceeding RPM limits immediately.

The solution is asynchronous queueing. Instead of processing LLM requests synchronously in the HTTP request thread, you push them into a message queue (e.g., Redis, RabbitMQ, or AWS SQS).

  1. API Layer: The backend receives the user request, validates it, and pushes it to the queue. It immediately returns a 202 Accepted status with a job_id.
  2. Worker Layer: A background worker consumes jobs from the queue. It has its own retry logic and backoff strategy.
  3. Result Storage: The worker stores the result in a database or cache, linked to the job_id.
  4. User Retrieval: The user polls an endpoint (GET /jobs/{job_id}) or receives a WebSocket notification when the result is ready.

This architecture decouples the user’s expectation from the LLM’s speed. The LLM provider sees a steady, manageable stream of requests from the worker, rather than a burst from thousands of users. This is a fundamental pattern for building scalable AI applications.

Architecture diagram showing an async message queue pattern. Clients send requests to API, which pushes to a queue. Workers consume from queue and call LLM. Clean, technical style, arrows showing data flow.

Graceful Degradation Strategies

Even with retries and queueing, LLMs can fail due to prolonged outages, severe throttling, or content filter blocks. A stable system does not simply return a 500 error. It implements graceful degradation.

Graceful degradation means providing a reduced but functional service experience when the primary system is impaired. In LLMOps, this can take several forms:

  1. Model Fallback: If the primary model (e.g., GPT-4) is rate-limited, automatically fall back to a smaller, faster, and cheaper model (e.g., GPT-3.5-turbo or a local open-source model). The response may be less sophisticated, but it is still useful.
  2. Cached Responses: For repetitive queries, serve a cached response from the last successful generation. This is particularly effective for FAQ-style interactions.
  3. Partial Streaming: If the full response is delayed, stream a preliminary summary or a placeholder message to keep the user engaged.
  4. Human-in-the-Loop: For critical tasks, if the AI fails, route the request to a human operator via a ticketing system.

Implementing these strategies requires explicit code paths. For example:

def generate_response(prompt):
    try:
        return call_llm_with_retry(prompt, model="gpt-4")
    except Exception as e:
        logger.warning("GPT-4 failed, falling back to GPT-3.5")
        try:
            return call_llm_with_retry(prompt, model="gpt-3.5-turbo")
        except Exception as e2:
            logger.error("Both models failed, returning cached response")
            return get_cached_response(prompt) or "We are experiencing high demand. Please try again later."

This approach ensures that your application remains available and useful, even when the underlying AI infrastructure is unstable.

Pitfalls

Common mistakes in implementing rate limit handling often undermine the stability you are trying to build. First, ignoring the Retry-After header entirely can lead to unnecessary wait times if the provider provides a specific window, though it should never be the sole mechanism. Second, using fixed delays without jitter is a frequent error; as mentioned, this causes synchronized retry storms that defeat the purpose of retrying. Finally, failing to handle the final retry failure gracefully results in unhandled exceptions bubbling up to the user interface, creating a poor experience. Always ensure that your retry logic has a definitive end state and a fallback path.

Takeaways

To effectively manage API constraints in LLMOps, focus on three core mechanisms. First, implement exponential backoff with jitter to respect provider limits while avoiding synchronized retry storms. Second, utilize asynchronous queueing to decouple user traffic from inference workloads, smoothing out bursts and protecting your RPM/TPM budgets. Third, design for graceful degradation by implementing fallback models, caching, and clear error states so that users always receive a response, even if it is not the ideal one.

FAQ

Q: What is the difference between a 429 error and a 500 error? A: A 429 error indicates that you are sending requests too fast (rate limiting), while a 500 error indicates a server-side failure or bug. 429 is a flow-control signal; 500 is an error state.

Q: Should I retry immediately after receiving a 429? A: No. Immediate retries often exacerbate the problem by adding more load to an already throttled system. Use exponential backoff with jitter to space out retries.

Q: How do I handle rate limits if I have thousands of concurrent users? A: Client-side retries are insufficient for high concurrency. You should implement an asynchronous queue (like Redis or RabbitMQ) to buffer requests and process them at a controlled rate via background workers.

Conclusion

Handling 429 errors in LLMOps is not about fixing bugs; it is about designing for reality. LLM providers are shared resources with strict limits. By implementing exponential backoff with jitter, you respect those limits. By using queueing, you smooth out traffic spikes. By planning for graceful degradation, you ensure that your users always have a path forward, even when the AI is unavailable. These are not optional optimizations; they are the foundation of reliable AI systems.

Related posts