n4nAI

Rate limits and concurrency for high-volume agent workloads

Practical guide to managing rate limits concurrency AI agent API at scale: backoff, pooling, fallback, and routing patterns for reliable agent workloads.

n4n Team5 min read1,140 words

Audio narration

Coming soon — every post will get a voice note here.

Most agent stacks fall over not because the model is slow, but because the rate limits concurrency AI agent API contract wasn’t designed for bursty, multi-step tool use. A single user turn can fan out into dozens of parallel LLM calls, and naive clients hit 429s within minutes. This guide lays out an ordered path to build throughput without sacrificing latency or cost.

1. Map the agent’s call graph before tuning limits

You cannot set a concurrency number until you know how many LLM requests a single agent task spawns. A typical ReAct loop issues one completion per reasoning step, but many implementations run tool calls in parallel or batch multiple summaries at once. Sketch the tree: root planner call, branch to N tool-result compressions, each of which may call the model again.

If you skip this step you will either over-provision semaphores and get throttled at the provider, or under-provision and serialize work that should be parallel. Measure with a single debug run: log every chat.completions.create with a parent span id and a task id.

Common pitfall: treating the agent as one sequential request. In reality, a “turn” might be 40 calls. Your rate limits concurrency AI agent API budget must cover the fan-out, not just the user-facing latency target. A batch ingestion job that triggers 200 agents simultaneously multiplies that fan-out by 200, so the call graph is the unit of capacity planning.

2. Cap concurrency per process with a semaphore

Start with a local guard. In Python asyncio, a semaphore is the minimal primitive:

import asyncio

MAX_CONCURRENT = 20  # tune against provider limit and worker count
sem = asyncio.Semaphore(MAX_CONCURRENT)

async def call_llm(client, payload):
    async with sem:
        return await client.chat.completions.create(**payload)

This bounds simultaneous in-flight requests per worker. If you run eight workers, global concurrency is 8 * 20 = 160. That might already exceed a provider’s requests-per-minute (RPM) limit depending on tier.

Tradeoff: a low cap protects you from 429s but increases tail latency when the agent fans out. A high cap improves throughput until the provider starts rejecting. Set the initial value to 30–50% of your estimated safe RPM divided by worker count, then adjust from error rates.

The rate limits concurrency AI agent API behavior is not uniform: some providers count tokens-per-minute (TPM) separately. A semaphore on request count ignores a 100k-token batch that consumes your TPM in one shot. Extend the guard to also check an estimated token budget if your prompts are large. For example, before acquiring the semaphore, compute estimated_tokens = prompt_tokens + max_completion; if a separate TPM bucket lacks headroom, block or shed load.

3. Retry with capped exponential backoff and jitter

A 429 is not a failure; it is a signal to slow down. Use bounded retries with full jitter:

from tenacity import retry, stop_after_attempt, wait_random_exponential

@retry(
    stop=stop_after_attempt(4),
    wait=wait_random_exponential(multiplier=0.5, max=20),
    retry=lambda r: r.exception is not None or r.result().status_code == 429
)
async def safe_call(client, payload):
    return await call_llm(client, payload)

Cap total retry time well below your user-facing deadline. If the agent step has 10 seconds to complete, do not allow 30-second backoff chains. Pass a deadline parameter and check remaining time before each attempt.

Pitfall: retry storms. When a provider degrades, every worker retries simultaneously and amplifies load. Combine retries with the semaphore and a shared token bucket (next section) so that retries consume the same global quota. Never retry a non-idempotent write call without tracing. For read-only completions, retries are safe; for agent actions that mutate external state, retry only after confirming the prior call did not execute.

4. Coordinate limits across workers with a shared token bucket

Per-process semaphores are blind to siblings. Stand up a Redis-backed token bucket that all workers hit before issuing a call:

import redis, time

r = redis.Redis()

def acquire(token_key="llm_rpm", capacity=200, refill=200/60):
    # simplistic atomic take; production needs Lua script
    now = time.time()
    pipe = r.pipeline()
    pipe.hgetall(token_key)
    # ... compute tokens, deduct one, write back
    # omitted for brevity
    return True  # or False if empty

A proper implementation uses a Lua script to atomically compute tokens = min(capacity, tokens + elapsed*refill) and deduct if positive. This gives you global RPM/TPM enforcement across a horizontal fleet.

Tradeoff: network round-trip per call adds ~1ms. Acceptable versus a provider ban. For batch jobs, use a separate bucket with higher capacity but stricter TPM accounting. Keep the bucket key scoped to model family—small models can have 10x the RPM of large ones—so you don’t starve expensive calls behind a cheap call flood.

5. Route to avoid provider-specific throttling

Different backends enforce different ceilings. An OpenAI-compatible gateway can let you express routing intent without branching code. Example request body:

{
  "model": "gpt-4o-mini",
  "messages": [{"role": "user", "content": "Extract fields"}],
  "route": {"prefer": ["openai", "azure-openai", "anthropic"]}
}

When you design for rate limits concurrency AI agent API at scale, treat provider selection as a dynamic lever. If one backend returns 429, the gateway should shift traffic. n4n.ai honors client routing directives and forwards provider cache-control hints, which matters when you want cached prefixes across fallback providers. Automatic fallback when a provider is rate-limited or degraded absorbs transient errors, but you still need the client-side caps from steps 2–4 to prevent a thundering herd the moment fallback kicks in.

Pitfall: assuming fallback is free. Cross-provider fallback may change latency profile or token accounting. Keep a timeout on the gateway call and a local circuit breaker that stops routing to a backend after N consecutive failures.

6. Leverage cache-control to cut redundant spend

Agents resend system prompts, tool schemas, and few-shot examples on every step. Providers like Anthropic support cache_control on messages; OpenAI-compatible gateways can forward those hints. Mark static prefixes:

{
  "messages": [
    {"role": "system", "content": "You are a tool-using agent. Schema: ...", "cache_control": {"type": "ephemeral"}}
  ]
}

This reduces both token spend and TPM pressure, effectively raising your real concurrency headroom. Tradeoff: cached tokens may have a minimum lifetime and only help if the prefix is byte-identical. Dynamically generated tool lists break caching. Build your system prompt from fixed templates and append volatile content after the cached boundary.

7. Meter per-token usage and alert on anomalies

Every response carries usage. Ship it to your metrics system:

resp = await safe_call(client, payload)
pt = resp.usage.prompt_tokens
ct = resp.usage.completion_tokens
metrics.histogram("llm.prompt_tokens", pt, tags={"model": payload["model"]})
metrics.histogram("llm.completion_tokens", ct)

Per-token metering exposes runaway agents that loop or emit verbose JSON. Set alerts on per-agent token velocity, not just error rate. If a single task exceeds 50k tokens unexpectedly, kill it. A gateway that aggregates usage across providers on one endpoint simplifies this, but the key is tagging by task id so you can trace which agent path burned the quota.

Common pitfalls and tradeoffs

  • Request vs token limits: RPM limits are easy to hit; TPM limits silently throttle large prompts. Track both.
  • Interactive vs batch isolation: A background summarization job should not starve a user-facing agent. Use separate buckets or queues.
  • Retries without backpressure: A 429 retry that ignores global quota multiplies load.
  • Fallback latency: Automatic provider switching adds tail latency; set deadlines.
  • Cache invalidation: Changing a single character in a cached prefix defeats the optimization.
  • Single global cap: One semaphore for all models ignores that a 70B model and a 7B model have wildly different quotas.

Final checklist

  1. Draw the call graph; count max fan-out per turn and per batch.
  2. Set per-worker semaphore to ~40% of safe RPM/worker, split by model class.
  3. Add bounded jittered retries with deadline propagation.
  4. Implement Redis token bucket for fleet-wide RPM/TPM with Lua atomicity.
  5. Send routing directives; rely on gateway fallback but keep client caps.
  6. Mark static prefixes with cache-control; keep volatile content after the boundary.
  7. Export per-token usage tagged by task; alert on velocity anomalies.

Following this ordered path moves you from reactive 429 handling to predictable throughput for high-volume agent workloads.

Tagsrate-limitsconcurrencyai-agentsscaling

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All best api for ai agents & tool use posts →