Rate Limits
Per-organization RPM and TPM limits, voice limits, response headers, and recommended backoff patterns.
Xantly enforces per-organization rate limits using a distributed sliding window algorithm. Limits are applied per endpoint category and per minute.
How rate limiting works
- Sliding window: enforced per organization per endpoint via Redis atomic Lua scripts
- Dual enforcement for inference: chat completions and embeddings check both RPM (requests per minute) and TPM (tokens per minute) simultaneously to prevent quota burn attacks
- Other endpoints: RPM only
Default limits by endpoint
| Endpoint category | Default RPM | Notes |
|---|---|---|
Inference (/v1/chat/completions, /v1/embeddings) | 300 | Both RPM and TPM enforced. Your own limit is echoed on every response in x-ratelimit-limit; read that rather than assuming this default. |
Voice (/v1/voice/*) | Per-organization | RPM + monthly minutes + concurrent sessions (see below) |
| Governance writes | 5 | PUT/POST/DELETE on /v1/governance |
| Routing writes | 10 | PUT on /v1/routing |
| Formal specs | 50 | /v1/governance/formal-specs |
| Reliability writes | 100 | POST/PUT on /v1/reliability |
Rate limits are operational guardrails set per organization, not pricing tiers. Every organization starts with sensible defaults, and limits can be raised from the dashboard or through an enterprise contract. Check your dashboard for your current limits.
Voice rate limits
Voice endpoints (/v1/voice/transcribe, /v1/voice/synthesize, /v1/voice/chat, /v1/voice/stream, /v1/voice/realtime, /v1/voice/turn) have three independent enforcement dimensions, all evaluated on every request:
- Voice RPM: sliding-window requests-per-minute, identical mechanism to inference RPM but a separate counter
- Monthly audio minutes quota: total
voice_input_audio_mssummed across the calendar month - Concurrent voice sessions: distinct active sessions in the
voice:concurrent:{org_id}Redis ZSET
If any one of these is exceeded, the request returns 429 Too Many Requests (or 402 Payment Required for budget/credit exhaustion).
Per-org defaults and overrides
Every organization gets default voice limits (RPM, monthly audio minutes, concurrent sessions, and an idle-session concurrency TTL), and each one can be overridden per organization via org_settings:
| Setting | Column | Notes |
|---|---|---|
| Voice RPM | voice_rpm_limit (int) | NULL = use the default |
| Monthly minutes | voice_monthly_minutes_limit (int) | NULL = use the default |
| Concurrent sessions | voice_concurrent_session_limit (int) | NULL = use the default |
| Monthly budget cap | voice_monthly_budget_usd (numeric) | Hard $ ceiling regardless of minute usage |
The concurrency TTL is how long an idle voice session remains in the active set before the reaper releases its slot. Long-running streaming sessions get heartbeats from the WebSocket handler every turn so they aren't reaped mid-call. Voice limits are guardrails, not pricing: raising them never changes the metered rate.
Enforcement order
When a voice request arrives, Xantly evaluates the checks in this order. The first failure returns immediately:
- Monthly USD budget cap: same
budget:usage:{org}:general:{YYYY-MM}Redis pool as inference. Returns402 Payment Required. - Monthly voice minutes limit:
voice:audio_mins:{org}:{YYYY-MM}Redis key vs the org limit. Returns429. - Credit floor: accounts must have
credit_balance_cents >= 5($0.05) to start a request. Returns402. - Concurrent session limit: Lua script checks ZCARD against the org limit. Returns
429. - Voice RPM (sliding window): middleware-level RPM enforcement. Returns
429.
Cost-aware sub-pipeline
Inside the voice pipeline, the LLM stage uses BaRP (the existing routing fairness layer) with a VoiceOnly constraint that prefers models with avg_latency_ms < 300. This sub-pipeline does not consume the voice RPM bucket, only the outer voice request does.
Rate limit headers for voice
Voice endpoints return the same standard headers as other endpoints:
| Header | Voice meaning |
|---|---|
X-RateLimit-Limit | Voice RPM limit (org override or default) |
X-RateLimit-Remaining | Voice requests remaining in the current minute |
X-RateLimit-Reset | Unix timestamp (epoch seconds) when the voice RPM window resets |
RateLimit-Reset | Seconds remaining until the voice RPM window resets |
Retry-After | Set on 429, in seconds, when any voice limit is exceeded |
For per-request voice cost + model headers, see Voice Billing and Voice Agents.
Rate limit response headers
Every response includes headers showing your current rate limit status:
Every response carries two families of rate limit headers. They are not aliases of each other: the two reset headers use different units, and reading the wrong one will schedule your next attempt decades away or immediately.
| Header | Unit | Description |
|---|---|---|
X-RateLimit-Limit | count | Maximum requests allowed per minute |
X-RateLimit-Remaining | count | Requests remaining in the current window |
X-RateLimit-Reset | epoch seconds | Absolute Unix timestamp when the window resets |
RateLimit-Limit | count | Same value as X-RateLimit-Limit |
RateLimit-Remaining | count | Same value as X-RateLimit-Remaining |
RateLimit-Reset | delta seconds | How many seconds until the window resets |
RateLimit | structured | limit=..., remaining=..., reset=..., reset in delta seconds |
The X- family is the older GitHub-style convention. The unprefixed family
follows the IETF RateLimit header fields draft, which specifies delta seconds.
Which to use. Prefer RateLimit-Reset, because a delta needs no clock
agreement between your machine and ours. Prefer Retry-After over both when
it is present.
The window is rolling
The limiter uses a rolling 60 second window, not a fixed one that empties on a
clock boundary. On a successful request RateLimit-Reset marks the end of the
current window rather than counting down, so under sustained load it stays
pinned at 60 while RateLimit-Remaining moves. That is expected. On a 429
it is a genuine countdown to when capacity returns.
When a limit is exceeded
| Header | Unit | Description |
|---|---|---|
Retry-After | seconds | How long to wait before retrying |
X-Xantly-Throttle-Source | enum | Which ceiling you hit: plan, gateway or upstream |
X-Xantly-RateLimit-Dimension | enum | requests or tokens, when the plan limit was the cause |
X-Xantly-Throttle-Source is worth branching on. plan means you are over
your own limit and backing off will help. upstream means a model provider is
throttling us, and a different model may succeed immediately. gateway means
the gateway itself is at capacity.
Concurrency
Requests per minute is not the only ceiling. There is also a limit on how many requests may be in flight across the gateway at one time:
| Header | Unit | Description |
|---|---|---|
X-RateLimit-Concurrency-Limit | count | In-flight request ceiling |
X-RateLimit-Concurrency-Scope | enum | Always gateway: this ceiling is shared, not yours alone |
The scope label matters. Every other X-RateLimit-* header describes your own
quota; this one describes a shared resource, so spare capacity in it is not
headroom you own. Exceeding it returns a 429 with
X-Xantly-Throttle-Source: gateway.
If you are sizing a worker pool, size it by measuring where your own error rate starts rising, not by dividing a published number. Concurrency, not requests per minute, is usually what binds first on agent workloads.
Rate limit exceeded, 429 response
{
"error": {
"message": "Rate limit exceeded: 1000 requests per minute",
"type": "rate_limit_error",
"code": "rate_limit_exceeded"
}
}
Handling rate limits
Exponential backoff with jitter (recommended)
import time
import random
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["XANTLY_API_KEY"],
base_url="https://api.xantly.com/v1",
)
def chat_with_backoff(messages, max_retries=6):
for attempt in range(max_retries):
try:
return client.chat.completions.create(
model="auto",
messages=messages,
)
except Exception as e:
if "429" in str(e) or "rate_limit" in str(e):
wait = (2 ** attempt) + random.uniform(0, 1)
print(f"Rate limited. Waiting {wait:.1f}s...")
time.sleep(wait)
else:
raise
raise Exception("Max retries exceeded")
async function chatWithBackoff(messages, maxRetries = 6) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await client.chat.completions.create({ model: "auto", messages });
} catch (err) {
if (err?.status === 429) {
const wait = Math.pow(2, attempt) * 1000 + Math.random() * 1000;
console.log(`Rate limited. Waiting ${(wait / 1000).toFixed(1)}s...`);
await new Promise((r) => setTimeout(r, wait));
} else {
throw err;
}
}
}
throw new Error("Max retries exceeded");
}
Inspect headers before retrying
import httpx
response = httpx.post(
"https://api.xantly.com/v1/chat/completions",
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
json={"model": "auto", "messages": [{"role": "user", "content": "Hello"}]},
)
remaining = int(response.headers.get("X-RateLimit-Remaining", 0))
if remaining < 10:
print(f"Warning: only {remaining} requests remaining in this window")
Best practices
- Use
model: "auto": the gateway's intelligent routing can reuse cached responses and distribute load across providers, reducing your effective RPM. - Enable semantic caching (
xantly.enable_cache: true, default), cache hits don't count against your rate limits and are billed at a flat $0.25 per million tokens instead of the provider price. - Batch when possible: combine multiple inputs into a single embeddings request.
- Monitor headers: log
X-RateLimit-Remainingin production to catch approaching limits before they hit. - Use
service_tier: "batch": signals a cost/latency preference that may take advantage of off-peak capacity.
Next steps
- Billing & Credits, Prepaid credits and monthly budget caps
- Chat Completions, Main inference endpoint