LLM load balancing puts a router between your application and approved model endpoints. It distributes requests by a rule such as weights or observed load. Add a fallback chain to handle eligible route failures. Multiple API keys add capacity only when they have independently provisioned quota. Keep conversations on the same route when cache reuse is useful.
Start with the quota, not the key
A route is more than a credential. It has a provider, account or project, model, region and set of limits. Two routes can look separate in your configuration while drawing from the same upstream allowance.
As of October 2026, the quota boundaries for three major APIs are:
| API | Documented quota scope | What another key changes |
|---|---|---|
| OpenAI | Organization and project; some model families share limits | A second key in the same project shares the applicable project and organization limits. |
| Anthropic | Organization, with configurable workspace limits | Another workspace still sits under the organization limit. |
| Gemini Developer API | Project | Multiple keys in the same project share its rate limits. |
Assume two approved endpoints each have an independent allowance of 100 requests per minute, with no higher shared cap. Their nominal combined allowance is 100 + 100 = 200 requests per minute. If both keys instead draw on one shared 100 request-per-minute parent limit, the combined allowance remains 100. Actual throughput also depends on token limits, bursts and available capacity.

Inventory requests per minute, input and output tokens per minute, daily limits, concurrency and spending caps for each route. A 50/50 split by request count is not a 50/50 split by tokens if one route receives longer prompts. Ask for more capacity or pace a queue when a shared limit is the constraint. Our provider rate limits guide covers the quota dimensions in more detail.
Use authorized credentials and review the provider agreement before pooling accounts. OpenAI's business terms prohibit transferring API keys with third parties and circumventing rate or usage limits.
Here, key rotation means distributing calls across credentials. Security rotation means replacing and revoking credentials; you need a process for both.
Choose a routing rule
Load balancing chooses a route while options are healthy. A fallback chain specifies what to try after an eligible failure. They work together: a weighted policy can select between two branches, each with its own fallback.

| Strategy | Selection signal | Good starting use | Watch for |
|---|---|---|---|
| Round robin | Sequential turns across routes | Similar endpoints with similar capacity | It does not measure health or token use. |
| Weighted round robin | Sequential turns in configured proportions | A planned split across unequal capacity | Weights alone do not enforce quotas. |
| Weighted random | Probability based on configured weights | A planned split without a fixed sequence | Small samples and retries change the observed split. |
| Least busy | Fewest in-flight requests | Routes with variable request duration | Counters need to reflect work across your workers. |
| Usage based | Recent request or token usage | Routes with measured quota headroom | Shared parent limits still need separate accounting. |
| Latency based | Recent response measurements | Interactive traffic where speed is a priority | Define whether you care about time to first token, total time, or generation speed. |
| Fallback | An ordered list after an eligible error | A backup for a failed route | A backup sharing the failed quota may fail too. |
LiteLLM's routing documentation defines least busy using ongoing calls and describes usage-based selection. Router measurements are separate from the provider's authoritative limits. LiteLLM's simple-shuffle strategy uses weighted random selection, as shown in the version 1.106.2 implementation. It is not weighted round robin.
Only put routes in the same pool after checking context length, tool calls, structured output, streaming, data handling and task quality. A shared API format does not make two models interchangeable. For regional requirements, approve every primary and fallback inference endpoint. The gateway's region and the model's inference region are separate choices.
Three ways to balance LLM requests
| Approach | Best fit | What you own |
|---|---|---|
| Requesty | Managed routing across model and provider choices | The policy, eligible models and application behavior |
| Client-side rotation | A single process with two authorized keys | Selection, limits, retries and coordination |
| LiteLLM proxy | Self-hosting or multiple credentials for one provider | Proxy deployment, security, upgrades and shared state |
We make Requesty. For a self-hosted proxy or a pool of multiple customer keys from the same provider, LiteLLM is the better fit for these examples. Requesty's BYOK documentation specifies at most one key per provider.
Rotate two OpenAI keys in one process
Install the OpenAI Python SDK with pip install openai. Set OPENAI_KEY_A and OPENAI_KEY_B to your authorized keys in the environment. These variable names belong to this example; the code passes them explicitly to the SDK.
import os
from itertools import cycle
from openai import OpenAI
clients = cycle([
OpenAI(api_key=os.environ["OPENAI_KEY_A"], max_retries=0),
OpenAI(api_key=os.environ["OPENAI_KEY_B"], max_retries=0),
])
for _ in range(2):
response = next(clients).chat.completions.create(
model="gpt-6-luna",
messages=[{"role": "user", "content": "Say hello."}],
)
print(response.choices[0].message.content)If both calls succeed, this sends the first to A and the second to B. The OpenAI model documentation lists gpt-6-luna as an API model ID; your account needs access to it. This example disables SDK retries to make each selection explicit. An error stops the loop.
The counter is local to one process. Separate workers do not coordinate, and the snippet has no health checks, pacing, fallback or session affinity. Add those mechanisms or move selection to a shared router when the workload needs them.
Run a self-hosted LiteLLM proxy
This example creates one public alias with two OpenAI keys and one Anthropic route. The weights are 35, 35 and 30, so the intended starting selection probabilities are 35%, 35% and 30% among eligible routes. Those are probabilities, not quota reservations. GPT-6 Luna and Claude Sonnet 5 are different models; evaluate both before sending production requests to the pool.
Set OPENAI_KEY_A, OPENAI_KEY_B, ANTHROPIC_API_KEY and LITELLM_MASTER_KEY in the proxy's environment. The master key must start with sk-, according to LiteLLM's virtual key documentation. The two OPENAI_KEY names are local names for this configuration.
Save the following as config.yaml. The model paths follow LiteLLM's OpenAI and Anthropic provider configuration, and weight sits inside litellm_params as in the routing documentation.
model_list:
- model_name: support-pool
litellm_params:
model: openai/gpt-6-luna
api_key: os.environ/OPENAI_KEY_A
weight: 35
model_info:
id: openai-a
- model_name: support-pool
litellm_params:
model: openai/gpt-6-luna
api_key: os.environ/OPENAI_KEY_B
weight: 35
model_info:
id: openai-b
- model_name: support-pool
litellm_params:
model: anthropic/claude-sonnet-5
api_key: os.environ/ANTHROPIC_API_KEY
weight: 30
model_info:
id: anthropic-a
router_settings:
routing_strategy: simple-shuffle
general_settings:
master_key: os.environ/LITELLM_MASTER_KEYInstall and start the proxy on loopback:
pip install 'litellm[proxy]==1.106.2'
litellm --config config.yaml --host 127.0.0.1The example uses Python 3.10 through 3.14 and is pinned to LiteLLM 1.106.2. An October 21 security report identified compromised 1.107.0 wheels and listed the 1.106.2 wheels among those inspected for that incident. For production, review current advisories, pin a reviewed release and verify artifact provenance. Keep the proxy on a private network, use TLS, and issue scoped keys to applications instead of sharing the master key.
In another shell with the master key set, send a smoke test:
curl -i http://localhost:4000/v1/chat/completions \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H 'Content-Type: application/json' \
-d '{"model":"support-pool","messages":[{"role":"user","content":"Say hello."}],"max_completion_tokens":1024}'Inspect x-litellm-model-id and the response. LiteLLM documents this header as the deployment identifier. Repeat with independent requests to see selections; a short run will not reproduce exact percentages. The output cap is an upper bound, including any billable reasoning tokens.
Add and test a LiteLLM fallback
The configuration above is a weighted pool. To add a separate backup, set GEMINI_API_KEY and append this entry to the existing model_list. The model and credential syntax follow LiteLLM's Gemini guide. Check the backup's access, quota, data rules and task quality first.
- model_name: support-backup
litellm_params:
model: gemini/gemini-3.8-flash
api_key: os.environ/GEMINI_API_KEY
model_info:
id: gemini-backupReplace the existing router_settings block with this one, keeping the original general_settings block:
router_settings:
routing_strategy: simple-shuffle
num_retries: 2
fallbacks:
- support-pool: [support-backup]Save config.yaml, stop the original proxy with Ctrl+C, and restart it with litellm --config config.yaml --host 127.0.0.1 so the backup configuration takes effect.
The LiteLLM fallback guide describes moving to the backup group after the primary call and its retries fail. A retry can pick another deployment in the primary pool; it does not guarantee that every primary was tried. Regular fallbacks cover more than transient errors, so review which failures should reach a backup, including content-policy failures.
Run an isolated 429 and 503 fallback drill
This local test uses fake upstream credentials and a stub that speaks the OpenAI-compatible API. All three weighted primary rows point to the stub, so any selected primary returns the staged error. The backup is a separate alias served by the same stub. The configuration follows LiteLLM's OpenAI-compatible endpoint guide and fallback configuration.
Stop the earlier proxy before starting this drill so port 4000 is free. Save this separate file as stage.yaml:
model_list:
- model_name: support-pool
litellm_params:
model: openai/gpt-6-luna
api_base: http://127.0.0.1:9999/v1
api_key: sk-stage-only
weight: 35
model_info:
id: stage-a
- model_name: support-pool
litellm_params:
model: openai/gpt-6-luna
api_base: http://127.0.0.1:9999/v1
api_key: sk-stage-only
weight: 35
model_info:
id: stage-b
- model_name: support-pool
litellm_params:
model: openai/gpt-6-luna
api_base: http://127.0.0.1:9999/v1
api_key: sk-stage-only
weight: 30
model_info:
id: stage-c
- model_name: support-backup
litellm_params:
model: openai/gpt-6.1-terra
api_base: http://127.0.0.1:9999/v1
api_key: sk-stage-only
model_info:
id: stage-backup
router_settings:
routing_strategy: simple-shuffle
num_retries: 0
fallbacks:
- support-pool: [support-backup]
general_settings:
master_key: os.environ/LITELLM_MASTER_KEYSave the stub as stub.py:
import json
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
model = body.get("model")
backup = model == "gpt-6.1-terra"
if backup and os.environ.get("FAIL_BACKUP") != "1":
status = 200
result = {
"id": "chatcmpl-stage", "object": "chat.completion",
"created": 1, "model": model,
"choices": [{"index": 0, "finish_reason": "stop",
"message": {"role": "assistant", "content": "backup"}}],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
}
else:
status = int(os.environ.get("PRIMARY_STATUS", "429"))
result = {"error": {"message": "staged upstream error",
"type": "rate_limit_error" if status == 429 else "server_error"}}
data = json.dumps(result).encode()
print(f"model={model} status={status}", flush=True)
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
ThreadingHTTPServer(("127.0.0.1", 9999), Handler).serve_forever()Run the stub in one shell:
PRIMARY_STATUS=429 python stub.pyIn a second shell, use a staging-only proxy key and start the staged configuration:
export LITELLM_MASTER_KEY=sk-stage-only-proxy-key
litellm --config stage.yaml --host 127.0.0.1In a third shell, set the same staging key and call the staged proxy:
export LITELLM_MASTER_KEY=sk-stage-only-proxy-key
curl -i http://localhost:4000/v1/chat/completions \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H 'Content-Type: application/json' \
-d '{"model":"support-pool","messages":[{"role":"user","content":"Say hello."}]}'Look for a primary gpt-6-luna 429 followed by a gpt-6.1-terra 200 in the stub logs. The response should contain backup, x-litellm-model-id: stage-backup and x-litellm-attempted-fallbacks: 1. The header reference identifies the deployment and counts fallback attempts. Use the stub logs to see the initial error; the final deployment ID alone does not show the full attempt sequence.
Stop the stub, restart it with PRIMARY_STATUS=503 python stub.py, and repeat the curl. Then restart it with PRIMARY_STATUS=503 FAIL_BACKUP=1 python stub.py and confirm that an error reaches the client when the backup fails too. Restart the staging proxy between experiments if cooldown state affects the next run.
With num_retries: 0, the first failed primary selection can go straight to the backup while the other primary entries remain eligible. Repeat the test with your intended retry settings and provider-shaped faults in staging. LiteLLM strips its fallback mock flags from proxy requests, so the drill sends HTTP errors from the stub instead. Never direct real provider credentials to a test server.
Stop the staging proxy and stub. In a shell with your original OPENAI_KEY_A, OPENAI_KEY_B, ANTHROPIC_API_KEY, GEMINI_API_KEY and secret LITELLM_MASTER_KEY set, restart the original proxy with litellm --config config.yaml --host 127.0.0.1. Do not use the staging master key for the original proxy.
For multiple proxy instances, LiteLLM documents Redis settings for shared routing state. Configured rpm and tpm values inform selection by default; the documented enforce_model_rate_limits check adds enforcement, with token enforcement best effort. Neither turns a shared provider quota into independent quotas.
Create a managed Requesty policy
Requesty's managed gateway lets you configure weighted provider and model routes without operating the proxy. You can use a Requesty-provided key or your own supported provider credential for each model. Requesty is free to get started; pay as you go adds a 5% fee on top of credit top-ups, with no seat fee or minimum spend. See pricing.
The following example uses a 70/30 split and an explicit conversation trace, following the load-balancing policy guide.
- 1Create your Requesty key
Sign up, create a Requesty API key, and add credits for the paid models in this example. To use your provider credentials, add them under Bring Your Own Keys. The BYOK documentation specifies at most one key per provider. The free tier covers free models; see Requesty pricing.
- 2Create the weighted policy
Open Routing Policies, select
Create Policy, chooseLoad balance, and name itsupport-split. Addopenai/gpt-6-lunaat 70% andanthropic/claude-sonnet-5-5at 30%. ChooseRequesty provided keyorMy own keyfor each entry, then save. Check current availability in the Requesty model library and confirm both models meet your workload and data requirements.For example, the Claude Sonnet 5.5 API rejects
temperature, while GPT-6 Luna supports Chat Completions function calling only withreasoning_effortset tonone. Test the parameters and tool behavior your application sends to both routes. - 3Call the policy
Set
REQUESTY_API_KEYin your environment and use the OpenAI Python SDK:Pythonimport os from openai import OpenAI client = OpenAI( api_key=os.environ["REQUESTY_API_KEY"], base_url="https://router.requesty.ai/v1", ) response = client.chat.completions.create( model="policy/support-split", messages=[{"role": "user", "content": "Say hello."}], extra_body={"requesty": {"trace_id": "session-123"}}, ) print(response.choices[0].message.content)
Use a distinct application-owned trace_id for each conversation and reuse it on later turns. Include the conversation history needed by your application. To measure distribution, send requests with many distinct traces and inspect the policy's Live Performance panel and analytics.
For failover, create fallback policies with approved backup models, then select those policies as the targets of a load-balancing policy. The example above selects individual models and does not configure that chain. If every route fails, return an error to the application.
For a speed-led policy instead, create a policy named support-fast, choose Latency, add compatible candidates, and call it with model="policy/support-fast". Requesty's latency routing considers response start time, generation speed and recent observations, penalizes recent errors, and tries candidates in speed order on failure. It does not produce a fixed percentage split or select by price. See our routing policies guide for more on composing policies.
Keep conversations on a useful route
OpenAI's caching guide requires a matching rendered prefix and an available cache entry on the selected machine. Its caches are not shared across organizations or regional processing boundaries. A second key in the same organization does not, by itself, establish a separate cache.

Put stable instructions and tool definitions before changing conversation content. For the Requesty Python example, enable provider-side prompt caching by adding "auto_cache": True alongside "trace_id" inside the requesty object in extra_body. Requesty's auto-caching guide explains how it adds breakpoints for providers that need them. Providers have minimum cacheable prefix lengths and different cache rules; check cache-read usage for each route. Auto caching uses provider-level prompt caching, not stored responses, so continue to send the required history.
A failure, policy change, expired entry or different eligible endpoint can change the result.
Affinity also changes the split you observe. Assume 100 sessions are assigned using 70/30 weights. One session assigned to the 70 side sends 1,000 requests, and each of the other 99 sessions sends one. That one session contributes 1,000 of 1,099 requests, about 91%. Even a 70/30 split of sessions would not yield a 70/30 split of requests. Measure tokens and spend too, since neither has to match the session split.
There is a correctness issue as well as a cache issue. LiteLLM's Responses API guidance explains that encrypted reasoning may be tied to the originating API key. Check its affinity option and your proxy version before moving a conversation with encrypted state between deployments.
Test the failure path
A successful hello request proves that one route answered. It does not prove that a backup is eligible or that your retry budget is bounded.
LLM load balancing rollout checks
0 of 7 done
For a 429, inspect the response before changing weights. OpenAI's rate-limit guide distinguishes temporary throttling from quota or billing errors that need an account fix. If Retry-After is present and valid, wait at least that long for a retry to the affected route. If it exceeds your deadline, defer the request. Without a valid hint, use bounded exponential backoff with jitter for transient errors. Our 429 diagnosis guide covers the response details.
Check the origin when Requesty returns an error. Its error reference distinguishes router from provider. A provider 429 may be helped by an approved route with independent capacity. A Requesty in-flight concurrency 429 occurs before the request is routed: bound client concurrency and wait for a slot. Requesty does not queue that rejection or impose its own requests-per-minute limit.
Retries multiply when the SDK, gateway and application each have their own loop. Set an overall attempt and elapsed-time budget, including the SDK's automatic retries. Do not blindly replay a stream after output or a tool call has reached the client. A timeout also leaves open the possibility that the upstream processed the request; protect side effects from duplication.
Finally, a second provider behind one gateway does not protect against a gateway outage. If that failure mode is in scope, design and test an application-level alternate path with the same model, data, region and accounting controls.
Put the first route into production
Start with one workload and inventory its actual quota boundaries. Run the same test set against each eligible route, configure the split, and inject a failure before increasing traffic. For a managed implementation, create a Requesty load-balancing policy and add fallback policies for the errors your application needs to survive.
Frequently asked questions
- How do I load balance requests across multiple LLM providers?
- Put a router between your application and a set of approved provider endpoints. Choose a distribution strategy, configure fallback for eligible failures, and test each model for quality and feature compatibility.
- Do multiple OpenAI API keys increase rate limits?
- OpenAI sets rate limits at the organization and project levels, and some model families share limits. Multiple keys drawing on the same applicable quota do not increase it; independent provisioned capacity is required to add allowance.
- How do I load balance two OpenAI API keys?
- A single application process can alternate between two OpenAI clients, each configured with an authorized key. A self-hosted LiteLLM proxy can also place multiple OpenAI credentials in a weighted deployment pool; check whether their quotas are independent.
- What is the difference between load balancing and fallback?
- Load balancing distributes requests among eligible routes during normal operation. Fallback tries another configured route after a failure; the two strategies can be combined.
- Can Requesty balance multiple API keys from the same provider?
- Requesty balances traffic across configured model and provider routes and supports customer-provided credentials. Its BYOK documentation specifies at most one key per provider, so use client-side rotation or a self-hosted proxy for a pool of multiple customer OpenAI keys.
- Why am I still getting 429 errors after load balancing?
- The routes may share a provider quota, exhaust a token or spending limit, or exceed gateway concurrency. Inspect the error origin and body, check any Retry-After header, and fix or pace the specific limit before retrying.
- Does load balancing affect prompt caching?
- It can. Changing providers, organizations or regions can move a conversation away from its existing cache. Keep a stable session route and an unchanged eligible prompt prefix, then measure cache reads because affinity alone does not guarantee a hit.
- JAN '26
Routing policies 101: fallback, load balancing, and latency in production
The three routing-policy primitives every LLM gateway needs (fallback chains, weighted load balancing, and latency-based selection) and when to use each. Written for teams deploying multi-model production setups.
- SEP '26
429 Too Many Requests: Causes and Fixes for LLM APIs
Fix 429 too many requests errors with provider-specific headers, billing checks, Retry-After parsing, bounded Python and TypeScript retries, and fallbacks.
- JAN '25
Switching LLM Providers: Why It’s Harder Than It Seems
