Rate Limits & Quotas
BVE Gateway enforces per-key rate limits using a Cloudflare Durable Object (ApiKeyLimiter). Each API key gets its own DO instance, so limits are applied independently per key.
Limit types
Section titled “Limit types”| Limit | Scope | Default | Configurable | Enforced |
|---|---|---|---|---|
| RPM | Per minute | 60 | Yes (per key) | Yes |
| RPD | Per day | 10,000 | Yes (per key) | Yes |
| Monthly requests | Per calendar month | None | Yes (per key) | Yes |
| Monthly tokens | Per calendar month | None | Yes (per key) | Yes |
RPM, RPD, and monthly request limits are enforced by the ApiKeyLimiter Durable Object. The monthly_token_limit field is enforced as a pre-flight D1 check before the DO call: if the key’s accumulated total_tokens for the current month meets or exceeds the limit, the request is rejected with 429 rate_limit_exceeded.
Defaults are set at key creation and can be customized via the rpm_limit, rpd_limit, monthly_limit, and monthly_token_limit fields in POST /admin/api-keys.
How rate limiting works
Section titled “How rate limiting works”- On each request, the auth middleware resolves the API key from D1.
- The quota middleware calls
ApiKeyLimiter.checkAndIncrement()on the DO instance for that key. - If any limit is exceeded, the request is rejected with
429. - If the DO call fails, the request is allowed (fail-open) to avoid blocking legitimate traffic.
Limit windows
Section titled “Limit windows”| Window | Resets |
|---|---|
| Minute | Rolling — each DO instance has a resetAt timestamp set 60 seconds ahead at window start |
| Day | UTC midnight |
| Month | First day of the next UTC month |
Rate limit error
Section titled “Rate limit error”When a limit is exceeded, the response is:
HTTP/1.1 429 Too Many RequestsContent-Type: application/jsonRetry-After: 42{ "error": { "message": "Rate limit exceeded: requests per minute", "type": "rate_limit_error", "param": null, "code": "rate_limit_exceeded" }}All 429 responses include a Retry-After header with the number of seconds until the rate-limit window resets. Clients should wait at least this many seconds before retrying.
Possible error messages:
"Rate limit exceeded: requests per minute"— RPM limit hit"Rate limit exceeded: requests per day"— RPD limit hit"Monthly request limit exceeded"— monthly request cap hit"Monthly token limit exceeded"— key’smonthly_token_limitreached
Worker-level caps
Section titled “Worker-level caps”In addition to per-key limits, the Worker has global request caps configured in wrangler.jsonc:
| Threshold | Value |
|---|---|
| Soft cap | 8,500,000 req/month |
| Hard cap | 9,500,000 req/month |
These thresholds are enforced by the globalRateLimiter middleware. Each Worker isolate maintains its own request counter; enforcement is per-isolate (not cluster-wide across all Cloudflare edge nodes). At the soft cap a warning is logged but the request continues. At the hard cap the middleware returns 503 capacity_exceeded immediately without forwarding to Fuelix.
Gateway rate limit headers
Section titled “Gateway rate limit headers”BVE Gateway adds the following headers to every allowed API response. They reflect the per-key limits configured in BVE Gateway, not the upstream Fuelix account capacity.
Per-minute (RPM) headers
Section titled “Per-minute (RPM) headers”| Header | Example | Description |
|---|---|---|
X-RateLimit-Limit-Requests | 60 | The key’s rpm_limit — max requests per minute |
X-RateLimit-Remaining-Requests | 57 | Requests remaining in the current minute window |
X-RateLimit-Reset-Requests | 42s | Seconds until the minute window resets |
Per-day (RPD) headers
Section titled “Per-day (RPD) headers”| Header | Example | Description |
|---|---|---|
X-RateLimit-Limit-Day | 10000 | The key’s rpd_limit — max requests per day |
X-RateLimit-Remaining-Day | 9843 | Requests remaining until UTC midnight |
X-RateLimit-Reset-Day | 38412s | Seconds until the next UTC midnight (day window reset) |
Monthly request headers
Section titled “Monthly request headers”These headers are only present when the key has a monthly_limit configured (the per-month request cap set at key creation). They reflect the count before the current request is counted, consistent with how all other X-RateLimit-Remaining-* headers behave.
| Header | Example | Description |
|---|---|---|
X-RateLimit-Limit-Month | 1000 | The key’s monthly_limit — max requests per calendar month |
X-RateLimit-Remaining-Month | 748 | Requests remaining before the monthly request cap is hit |
X-RateLimit-Reset-Month | 604800s | Seconds until the first day of the next UTC calendar month (when the counter resets) |
These headers are absent for keys with no monthly_limit set. When the monthly request limit is hit, the gateway returns 429 rate_limit_exceeded with message "Monthly request limit exceeded" and a Retry-After pointing to the start of next month.
Monthly token headers
Section titled “Monthly token headers”These headers are only present when the key has a monthly_token_limit configured. They reflect the pre-request token total — the current request’s tokens are counted asynchronously after the upstream response, so the remaining value is approximate (the same approach used by OpenAI’s own token rate-limit headers).
| Header | Example | Description |
|---|---|---|
X-RateLimit-Limit-Tokens | 50000 | The key’s monthly_token_limit — max tokens per calendar month |
X-RateLimit-Remaining-Tokens | 47832 | Tokens remaining before the monthly token limit is hit (pre-request count) |
X-RateLimit-Reset-Tokens | 691200s | Seconds until the start of the next UTC calendar month (when the counter resets) |
These headers are absent for keys with no monthly_token_limit set. When the monthly token limit is hit, the gateway returns 429 rate_limit_exceeded with Retry-After pointing to the start of next month.
These headers are only set on allowed (non-429) responses. If the Durable Object fails (fail-open path), no X-RateLimit-* headers are set.
All headers are exposed via CORS (Access-Control-Expose-Headers) so browser-based clients (e.g. OpenAI TypeScript SDK in a SPA) can read them.
Reading rate limit headers
Section titled “Reading rate limit headers”import OpenAI from 'openai';
const client = new OpenAI({ apiKey: 'sk-bve-YOUR_KEY', baseURL: 'https://api.bve.me/v1',});
// Use the raw fetch override to capture response headersconst rawClient = new OpenAI({ apiKey: 'sk-bve-YOUR_KEY', baseURL: 'https://api.bve.me/v1', fetch: async (url, init) => { const res = await globalThis.fetch(url, init); console.log('RPM remaining:', res.headers.get('x-ratelimit-remaining-requests')); console.log('RPD remaining:', res.headers.get('x-ratelimit-remaining-day') ?? '(no day header)'); console.log('Reset in:', res.headers.get('x-ratelimit-reset-requests')); return res; },});
await rawClient.chat.completions.create({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: 'hi' }],});import httpxfrom openai import OpenAI
class RateLimitLoggingTransport(httpx.HTTPTransport): def handle_request(self, request): response = super().handle_request(request) print("RPM remaining:", response.headers.get("x-ratelimit-remaining-requests")) print("Reset in: ", response.headers.get("x-ratelimit-reset-requests")) return response
client = OpenAI( api_key="sk-bve-YOUR_KEY", base_url="https://api.bve.me/v1", http_client=httpx.Client(transport=RateLimitLoggingTransport()),)
client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": "hi"}],)curl -si -X POST https://api.bve.me/v1/chat/completions \ -H "Authorization: Bearer sk-bve-YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}' \ | grep -i 'x-ratelimit'Example headers for a key with rpm_limit: 60, rpd_limit: 10000,
monthly_limit: 1000, and monthly_token_limit: 50000:
X-RateLimit-Limit-Requests: 60X-RateLimit-Remaining-Requests: 59X-RateLimit-Reset-Requests: 58sX-RateLimit-Limit-Day: 10000X-RateLimit-Remaining-Day: 9999X-RateLimit-Reset-Day: 38400sX-RateLimit-Limit-Month: 1000X-RateLimit-Remaining-Month: 748X-RateLimit-Reset-Month: 604800sX-RateLimit-Limit-Tokens: 50000X-RateLimit-Remaining-Tokens: 47832X-RateLimit-Reset-Tokens: 691200sThe X-RateLimit-*-Month headers only appear when monthly_limit (request count) is set on the key. The X-RateLimit-*-Tokens headers only appear when monthly_token_limit is set.
Handling 429 errors
Section titled “Handling 429 errors”All 429 responses include a Retry-After header (integer seconds). Use it to back off before retrying rather than using a fixed sleep.
import OpenAI from 'openai';
const client = new OpenAI({ apiKey: 'sk-bve-YOUR_KEY', baseURL: 'https://api.bve.me/v1', maxRetries: 0, // handle retries manually to respect Retry-After});
async function chatWithRetry(prompt: string, maxAttempts = 3) { for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await client.chat.completions.create({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: prompt }], }); } catch (err) { if (err instanceof OpenAI.RateLimitError) { const retryAfter = Number(err.headers?.['retry-after'] ?? 60); console.log(`Rate limited. Retrying in ${retryAfter}s (attempt ${attempt}/${maxAttempts})`); if (attempt === maxAttempts) throw err; await new Promise(r => setTimeout(r, retryAfter * 1000)); } else { throw err; } } }}
const result = await chatWithRetry('Hello!');console.log(result.choices[0].message.content);import timeimport openai
client = openai.OpenAI( api_key="sk-bve-YOUR_KEY", base_url="https://api.bve.me/v1", max_retries=0, # handle retries manually to respect Retry-After)
def chat_with_retry(prompt: str, max_attempts: int = 3) -> str: for attempt in range(1, max_attempts + 1): try: response = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": prompt}], ) return response.choices[0].message.content except openai.RateLimitError as e: retry_after = int(e.response.headers.get("retry-after", 60)) print(f"Rate limited. Retrying in {retry_after}s (attempt {attempt}/{max_attempts})") if attempt == max_attempts: raise time.sleep(retry_after)
print(chat_with_retry("Hello!"))#!/usr/bin/env bash# Retry a request up to 3 times, respecting Retry-After on 429attempt=0max_attempts=3
while [ $attempt -lt $max_attempts ]; do response=$(curl -si -X POST https://api.bve.me/v1/chat/completions \ -H "Authorization: Bearer sk-bve-YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Hello!"}]}')
status=$(echo "$response" | grep '^HTTP' | awk '{print $2}')
if [ "$status" = "429" ]; then retry_after=$(echo "$response" | grep -i '^retry-after:' | awk '{print $2}' | tr -d '\r') retry_after=${retry_after:-60} echo "Rate limited. Waiting ${retry_after}s before retry..." sleep "$retry_after" attempt=$((attempt + 1)) else echo "$response" | tail -1 # print response body break fidoneChecking remaining quota (response headers from Fuelix)
Section titled “Checking remaining quota (response headers from Fuelix)”If Fuelix includes quota headers in its response, they are forwarded:
| Header | Description |
|---|---|
x-quota-allowed | Whether quota was allowed (from Fuelix) |
x-quota-available | Remaining quota (from Fuelix) |
x-quota-reset | Quota reset time (from Fuelix) |
Upstream provider rate-limit headers
Section titled “Upstream provider rate-limit headers”The following headers are forwarded from Fuelix when present. They reflect the aggregate upstream Fuelix account limits, not the per-key BVE Gateway RPM/RPD/monthly limits documented above.
| Header | Description |
|---|---|
x-ratelimit-limit-requests | Upstream request rate limit |
x-ratelimit-limit-tokens | Upstream token rate limit |
x-ratelimit-remaining-requests | Upstream requests remaining in window |
x-ratelimit-remaining-tokens | Upstream tokens remaining in window |
x-ratelimit-reset-requests | Time until upstream request window resets |
x-ratelimit-reset-tokens | Time until upstream token window resets |
retry-after | Seconds to wait before retrying (forwarded from upstream 429/503) |
These headers are also exposed via CORS, so browser-based SDK clients (OpenAI TypeScript SDK in a SPA) can read them for pre-emptive backoff without waiting for a 429.
Provider-specific headers
Section titled “Provider-specific headers”The following provider-native headers are forwarded from Fuelix when present. They are useful for provider-specific SDK integrations and request tracking.
Anthropic Messages API headers
Section titled “Anthropic Messages API headers”These headers are returned by Anthropic’s API and forwarded to clients calling /v1/messages. They reflect the Fuelix account’s aggregate Anthropic quota, not per-key BVE Gateway limits.
Aggregate token headers
Section titled “Aggregate token headers”| Header | Description |
|---|---|
anthropic-ratelimit-requests-limit | Anthropic account’s requests-per-minute limit |
anthropic-ratelimit-requests-remaining | Requests remaining in the current Anthropic window |
anthropic-ratelimit-requests-reset | ISO 8601 timestamp when the Anthropic request window resets |
anthropic-ratelimit-tokens-limit | Anthropic account’s combined (input + output) tokens-per-minute limit |
anthropic-ratelimit-tokens-remaining | Combined tokens remaining in the current Anthropic window |
anthropic-ratelimit-tokens-reset | ISO 8601 timestamp when the Anthropic token window resets |
Per-direction token headers
Section titled “Per-direction token headers”Anthropic also exposes separate limits for input and output tokens. These are forwarded when present:
| Header | Description |
|---|---|
anthropic-ratelimit-input-tokens-limit | Anthropic account’s input (prompt) tokens-per-minute limit |
anthropic-ratelimit-input-tokens-remaining | Input tokens remaining in the current window |
anthropic-ratelimit-input-tokens-reset | ISO 8601 timestamp when the input token window resets |
anthropic-ratelimit-output-tokens-limit | Anthropic account’s output (completion) tokens-per-minute limit |
anthropic-ratelimit-output-tokens-remaining | Output tokens remaining in the current window |
anthropic-ratelimit-output-tokens-reset | ISO 8601 timestamp when the output token window resets |
Use the per-direction headers to distinguish input vs. output budget exhaustion — critical for accurate pre-emptive backoff in rate-limited Anthropic SDK apps (@anthropic-ai/sdk). A request that is heavy on output tokens may still have ample input token budget remaining, so acting on only the aggregate header can cause unnecessary backoff.
Groq and OpenAI diagnostic headers
Section titled “Groq and OpenAI diagnostic headers”| Header | Description |
|---|---|
x-groq-request-id | Groq’s internal request ID — use for support requests and log correlation on Groq’s side |
openai-processing-ms | OpenAI’s server-side processing time in milliseconds — useful for client-side latency attribution |
OpenRouter headers
Section titled “OpenRouter headers”These headers are returned by OpenRouter when your request is routed through it and are forwarded to the client.
| Header | Description |
|---|---|
x-openrouter-model | The actual model ID selected by OpenRouter after provider routing (e.g. openai/gpt-4o). Useful for confirming which model served a request when using OpenRouter’s automatic model selection. |
x-or-cache-status | OpenRouter semantic cache result: HIT if the response came from OpenRouter’s semantic cache, MISS if a fresh inference was run. Cache hits save tokens and reduce latency. |
x-or-remaining-tokens | Token budget remaining in the current OpenRouter rate-limit window. Analogous to x-ratelimit-remaining-tokens but specific to the x-or-* header family. |
All provider-specific headers are exposed via CORS (Access-Control-Expose-Headers) so browser-based SDK clients can read them from cross-origin JavaScript.