Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Handle LLM Rate Limits and Retries

By Kokil Thapa | Last reviewed: September 2026

Your app calls an LLM on every form submit, chat message, or document scan. The first hundred requests feel fine. Then traffic spikes, batch jobs overlap, and OpenAI or Anthropic returns HTTP 429. Users see timeouts. Costs spike from blind retries. To handle LLM rate limits and retries properly, you need more than a sleep loop in a controller. You need provider-aware throttling, structured backoff, and queue discipline baked into your architecture from day one.

What HTTP status codes and headers signal LLM rate limits?

Most hosted LLM APIs return 429 Too Many Requests when you exceed a quota. Some providers also return 503 Service Unavailable during overload. Treat both as transient for retry logic, but log them differently. A 429 usually means your account hit RPM, TPM, or RPD caps. A 503 often means regional capacity is strained.

Read the response body. OpenAI-style payloads include an error.type field such as rate_limit_exceeded or insufficient_quota. The second case is not a retry problem. Retrying a billing or quota failure wastes money and delays the real fix.

Check headers before you guess a backoff interval. The Retry-After header tells you how long to wait. It may be seconds or an HTTP date. Honor it when present. Fall back to exponential backoff only when the header is missing.

Provider docs define the limits you are actually fighting. Review the current OpenAI rate limits guide and your vendor tier before you tune workers. Limits differ by model, endpoint, and account tier. Embedding endpoints use token-per-minute budgets. Chat completions use both RPM and TPM.

LLM Request Flow With Rate Limit GateYour AppController / JobThrottle GateToken bucket / QueueRetry HandlerBackoff + jitterLLM API429 / 200429 Response PathParse Retry-After → wait → re-enter queueNever retry insufficient_quota errors
Handle LLM rate limits and retries by placing throttling and backoff before the provider, not inside the user-facing request cycle.

Common limit types you will hit

  • RPM — requests per minute. Batch importers blow this first.
  • TPM — tokens per minute. Long prompts plus big completions burn TPM fast.
  • RPD — requests per day on free or low tiers.
  • Concurrent requests — parallel workers count even when RPM looks fine.

Log limit type, model name, and approximate token count on every 429. That data drives capacity planning. It also feeds AI rate limits and cost optimization decisions later.

How do you implement exponential backoff for LLM API calls?

Naive retry loops cause retry storms. Ten workers each retry five times and you suddenly send fifty calls into a provider that already told you to slow down. Exponential backoff increases wait time after each failure. Jitter randomizes the delay so workers do not wake up together.

A practical formula: base delay × 2^attempt, capped at 60 seconds, plus random jitter up to 25% of the delay. Respect Retry-After when it exceeds your calculated value. Set a max attempt count. Three to five retries is typical for chat. Batch jobs can afford more because they run async.

In PHP 8.3+ on Laravel 12 or Laravel 13, wrap the HTTP call in a small service class. Keep retry policy out of Blade views and fat controllers. I follow the same pattern I use for payment gateways and SMS APIs on production apps.

<?php

namespace App\Services\Llm;

use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
use RuntimeException;

final class LlmClient
{
    private const MAX_ATTEMPTS = 5;

    public function chat(array $payload): array
    {
        $attempt = 0;

        while (true) {
            $attempt++;

            try {
                $response = Http::withToken(config('services.openai.key'))
                    ->timeout(120)
                    ->post('https://api.openai.com/v1/chat/completions', $payload)
                    ->throw();

                return $response->json();
            } catch (RequestException $e) {
                $response = $e->response;
                $status = $response?->status();

                if ($status === 429 && $attempt < self::MAX_ATTEMPTS) {
                    $this->sleepForRateLimit($response->header('Retry-After'), $attempt);
                    continue;
                }

                if ($status === 503 && $attempt < self::MAX_ATTEMPTS) {
                    $this->sleepForRateLimit(null, $attempt);
                    continue;
                }

                throw $e;
            }
        }
    }

    private function sleepForRateLimit(?string $retryAfter, int $attempt): void
    {
        if ($retryAfter !== null && ctype_digit($retryAfter)) {
            sleep((int) $retryAfter);
            return;
        }

        $baseMs = 500;
        $delayMs = min($baseMs * (2 ** ($attempt - 1)), 60_000);
        $jitterMs = random_int(0, (int) ($delayMs * 0.25));

        usleep(($delayMs + $jitterMs) * 1000);
    }
}

Never block a web request for sixty seconds while waiting on backoff. Return a queued job ID to the browser instead. Users on a legal-tech portal or eCommerce checkout will abandon the page long before the LLM responds. See streaming LLM responses with SSE for a better UX on chat features.

Exponential Backoff With JitterAttempt 1Wait ~0.5s+ jitterWait ~1s+ jitterWait ~2s+ jitterSuccessCap delay at 60s · Max 5 attempts · Honor Retry-AfterJitter spreads worker wake times across the windowRetry storms happen when every worker uses identical sleep
Exponential backoff with jitter is the core retry pattern when you handle LLM rate limits and retries in production workers.

Make retries safe with idempotency keys

LLM calls cost money. A retried chat turn can produce two billable completions for one user message. Store an idempotency key per user action. Check it before you call the provider. Persist the final response keyed to that ID.

This mirrors payment callback handling. On a client portal I built, document summarization runs in a queue with a UUID per upload batch. Retries reuse the same key. The user never sees duplicate summaries. Billing stays predictable.

How should you queue and throttle LLM requests in production?

Retries fix transient failures. Throttling prevents them. Put every non-trivial LLM call on a queue worker. Size concurrency to stay under your TPM budget with headroom. If your tier allows 60 RPM, do not run 60 parallel queue workers each firing one request per second.

Estimate tokens before you send when possible. Count input tokens with the provider tokenizer or a rough character heuristic. Reserve TPM budget in Redis before the HTTP call. Release unused reservation after the response returns. This pattern is simpler than it sounds and beats reactive 429 handling alone.

  1. Create a dedicated queue named llm with low concurrency.
  2. Track rolling RPM and TPM in Redis with a sliding window.
  3. Delay jobs that would exceed budget instead of sending and retrying.
  4. Alert when queue depth grows beyond a threshold for fifteen minutes.
  5. Route dead-letter failures to a manual review table.

Laravel's rate limiter works well for outbound API pacing. The same concepts apply as in rate limiting and API throttling in Laravel and Laravel rate limiting with custom keys. Use a custom key like llm:openai:tpm rather than per-user keys for provider caps.

<?php

use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Redis;

final class LlmThrottle
{
    public function acquire(int $estimatedTokens): void
    {
        while (! RateLimiter::attempt(
            'llm-rpm',
            50,
            fn () => true,
            60
        )) {
            usleep(200_000);
        }

        $bucket = 'llm:tpm:' . now()->format('YmdHi');

        while ((int) Redis::get($bucket) + $estimatedTokens > 90000) {
            usleep(500_000);
        }

        Redis::incrby($bucket, $estimatedTokens);
        Redis::expire($bucket, 120);
    }
}

Adjust the TPM ceiling to your actual tier. The number above is illustrative. Pull real limits from your provider dashboard. Pair throttling with monitoring described in LLMOps monitoring and guardrails.

Queue + Redis Throttle ArchitectureWeb / API LayerDispatch LlmJobRedis LimiterRPM + TPM windowsWorker PoolLow concurrencyProactive throttle beats reactive 429 retryJobs wait in queue when TPM bucket is fullUser gets 202 Accepted + job ID, not a hung page
Production systems handle LLM rate limits and retries best when Redis throttling sits ahead of the worker pool, not after repeated 429 errors.

Should you use multiple LLM providers to avoid rate limits?

Multi-provider routing helps when you need availability, not when you want to bypass a single quota you already maxed out. Each provider has its own billing, limits, and output quality profile. Fallback from OpenAI to Anthropic on 429 can keep a chatbot online during a spike. It also doubles your integration and eval surface.

Use a primary and secondary provider with explicit routing rules. Fail over only on 429 or 503, not on validation errors or content policy blocks. Log which provider served each request. Compare quality in LLM output evaluation pipelines before you automate failover in customer-facing paths.

StrategyBest forRiskComplexity
Single provider + queue throttleMost Laravel apps, predictable costHard stop when tier cap hitLow
Single provider + backoff retryLow-volume features, admin toolsRetry storms under loadLow
Multi-provider failoverHigh-availability chat, 24/7 support botsInconsistent answers, higher spendMedium
Self-hosted model overflowPrivacy-sensitive batch jobsGPU ops burden, latencyHigh

Self-hosted overflow via Ollama or vLLM is viable for internal tools. It is rarely the first fix for a public marketing site. Read running LLMs locally with Ollama and vLLM before you buy GPU hardware. For most Nepal SMB clients I work with, queue discipline plus a paid tier upgrade solves the problem faster than running local inference.

What production mistakes break LLM retry logic?

The failures I see in production are predictable. Teams retry non-idempotent writes. They ignore insufficient_quota. They run LLM calls synchronously inside HTTP requests. They set queue worker counts based on CPU cores instead of provider RPM.

Another common bug: retrying 400-level validation errors. If the prompt is too long, retrying will never succeed. Inspect status and error type before you sleep. Parse JSON error bodies defensively. Providers change field names occasionally.

Security matters too. Do not log full prompts containing PII when debugging 429 storms. Redact tokens and secrets. Follow protecting PII and secrets in LLM apps even in retry logs. Use the JSON formatter tool to inspect sanitized error payloads during development.

Retry or Fail? Decision TreeAPI Error429 or 503?Transient limit400 / 401 / 403?Fix input or authRetry with backoffHonor Retry-AfterFail fastNo retry loopinsufficient_quota → upgrade billing, never retry
Use a retry decision tree when you handle LLM rate limits and retries so workers only loop on genuinely transient errors.

Operational checklist before launch

  • Define max retries and log every attempt with latency and token count.
  • Move batch summarization and embedding jobs off the request thread.
  • Set billing alerts at 80% of monthly quota.
  • Document which features are safe to degrade when limits hit.
  • Test a synthetic 429 in staging with a mocked HTTP client.

These steps align with broader LLMOps ship and operate practice. They also reduce support tickets on apps like Mijar Law Associates where document AI must not block core portal workflows. For greenfield work, API development and custom software development engagements should spec retry behavior in the architecture doc, not as a post-launch patch.

Cost control and retry policy overlap heavily. Aggressive retries during a marketing campaign can burn a monthly LLM budget in hours. Read LLM cost optimization for production apps alongside this guide. The same third-party patterns from third-party API integration retry and backoff apply here. LLM vendors are just expensive, nondeterministic HTTP APIs.

Function-calling flows add another wrinkle. If the model returns tool calls, your retry must replay the whole turn consistently. Partial retries mid-tool-chain produce garbage state. See function calling and tool use with LLMs for design patterns that keep retries safe.

Key Takeaways

  • Parse 429 and 503 separately from quota and validation errors before any retry runs.
  • Use exponential backoff with jitter and honor Retry-After when the provider sends it.
  • Throttle outbound LLM calls with queues and Redis TPM tracking instead of hoping retries save you.
  • Attach idempotency keys to user actions so retried completions never duplicate billable work.
  • Keep LLM calls off the synchronous web path; return job IDs or stream via SSE for chat UX.
  • Log limit events and queue depth early—they predict billing overruns before finance notices.

People Also Ask

How many times should you retry an LLM API call?

Three to five attempts is a sensible default for background jobs with exponential backoff capped at sixty seconds. Interactive chat should retry at most twice or hand off to a queue. Always stop when the error type is insufficient quota or a 400-level validation failure.

What is the difference between RPM and TPM rate limits?

RPM counts HTTP requests per minute regardless of size. TPM counts tokens processed per minute across input and output. A single long document request can hit TPM while RPM still looks healthy. Size workers to both limits.

Should LLM retries be synchronous or queued?

Queue them. Synchronous retries in a web request tie up PHP-FPM workers and frustrate users. Dispatch a job, return 202 Accepted, and notify the client when complete. Synchronous retry belongs only in CLI scripts or admin one-offs.

Does caching reduce LLM rate limit pressure?

Yes. Cache identical prompt hashes when business rules allow it. Embeddings and FAQ answers are ideal cache candidates. Caching does not replace throttling, but it cuts duplicate spend and keeps you under TPM during traffic spikes.

Ship LLM features that survive real traffic

Handle LLM rate limits and retries as infrastructure, not a try/catch afterthought. Throttle before you call, backoff with jitter when the provider pushes back, and fail fast on errors that will never succeed. That combination keeps AI features stable on Laravel 12 and Laravel 13 apps without surprise API bills. If you want help wiring queues, provider failover, or LLMOps monitoring into a production product, contact us or explore AI integration and automation services. You can also browse the portfolio for examples of operational web systems that prioritize reliability over demo-day magic.

Frequently Asked Questions

Most hosted LLM APIs return HTTP 429 Too Many Requests when you exceed quota. Some return 503 Service Unavailable during overload. Treat both as transient for retry logic but log them differently. A 429 usually means your account hit RPM, TPM, or RPD caps. A 503 often means regional capacity is strained. Read the response body for error.type fields such as rate_limit_exceeded or insufficient_quota. Check the Retry-After header before guessing a backoff interval—it may be seconds or an HTTP date.

RPM counts HTTP requests per minute regardless of size. TPM counts tokens processed per minute across input and output.

Three to five attempts is typical for background jobs. Interactive chat should retry at most twice or hand off to a queue. Stop on insufficient_quota or 400-level validation failures.

Naive retry loops cause retry storms when multiple workers wake together. Use base delay multiplied by two to the power of attempt, capped at sixty seconds, plus random jitter up to twenty-five percent of the delay. Honor Retry-After when it exceeds your calculated value. In Laravel 12 or Laravel 13 on PHP 8.3 or higher, wrap the HTTP call in a dedicated service class—not controllers or Blade views. Set max attempts to three to five for chat; batch jobs can afford more because they run async.

Queue them. Synchronous retries tie up PHP-FPM workers and frustrate users waiting on backoff delays up to sixty seconds.

Put non-trivial LLM calls on a dedicated llm queue with low concurrency sized to your TPM budget with headroom. Track rolling RPM and TPM in Redis with a sliding window. Delay jobs that would exceed budget instead of sending and retrying. Use Laravel's RateLimiter with a custom key like llm:openai:tpm. Estimate tokens before sending, reserve TPM budget in Redis before the HTTP call, and release unused reservation after the response returns. Alert when queue depth grows beyond a threshold for fifteen minutes.

Retry-After tells you how long to wait after a rate limit response. It may be seconds or an HTTP date. Honor it when present. Fall back to exponential backoff with jitter only when the header is missing. If Retry-After exceeds your calculated backoff value, respect the header instead of your formula. Guessing sleep intervals without checking headers is a common source of retry storms under load.

No. insufficient_quota is a billing or quota failure, not a transient rate limit. Retrying wastes money and delays the real fix—upgrading your tier or addressing account limits. Parse the response body error.type field before any retry runs. A 429 with rate_limit_exceeded is retryable; insufficient_quota requires stopping retries immediately and fixing the underlying quota problem rather than sleeping and looping.

LLM calls cost money, and a retried chat turn can produce two billable completions for one user message. Store an idempotency key per user action—a UUID per upload batch works well for document summarization. Check the key before calling the provider and persist the final response keyed to that ID. Retries reuse the same key so duplicate completions never double-charge users or corrupt data. This mirrors payment callback handling on production apps.

Multi-provider routing helps availability, not bypassing a single quota you already maxed out. Fallback from OpenAI to Anthropic on 429 or 503 can keep a chatbot online during a spike, but it doubles integration and evaluation surface. Fail over only on 429 or 503, not validation errors or content policy blocks. Log which provider served each request. For most predictable-cost Laravel apps, single provider plus queue throttle is simpler and lower risk than automated failover.

Common failures include retrying non-idempotent writes, ignoring insufficient_quota, running LLM calls synchronously inside HTTP requests, sizing queue workers by CPU cores instead of provider RPM, and retrying 400-level validation errors like prompts that are too long. Partial retries mid-tool-chain in function-calling flows produce garbage state. Another bug is logging full prompts with PII during 429 debugging—redact tokens and secrets. Use a retry decision tree so workers only loop on genuinely transient errors.

Yes. Cache identical prompt hashes when business rules allow. Embeddings and FAQ answers are ideal candidates. Caching does not replace throttling but cuts duplicate spend and keeps you under TPM during traffic spikes.

Treat 503 as transient for retry logic like 429, but log them separately. A 503 often means regional capacity is strained rather than your account hitting RPM or TPM caps. Apply exponential backoff with jitter when Retry-After is absent. Do not confuse 503 overload with insufficient_quota billing failures found in the response body. Logging both status codes differently helps you distinguish account limits from provider-side capacity issues during incident review.

Users on a legal-tech portal or eCommerce checkout abandon pages long before a sixty-second backoff completes. Never block a web request while waiting on retry sleep. Dispatch a job, return 202 Accepted with a job ID, and notify the client when complete. For chat features, consider streaming via SSE rather than holding the connection during retries. Keep retry policy in queue workers, not PHP-FPM request cycles tied to user-facing form submits.

Log limit type, model name, approximate token count, latency, and retry attempt number on every 429. That data drives capacity planning and cost optimization decisions. Track queue depth and alert when it grows beyond a threshold for fifteen minutes. Set billing alerts at eighty percent of monthly quota. Route dead-letter failures to a manual review table. Never log full prompts containing PII when debugging 429 storms—redact tokens and secrets in retry debug logs.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: