Problem Summary
OpenAI enforces two distinct rate limit dimensions: Requests Per Minute (RPM) and Tokens Per Minute (TPM). When either limit is exceeded the API returns HTTP 429 with a RateLimitError. Applications that fire requests in tight loops, batch-process large documents, or share a single API key across multiple server instances commonly hit these walls without warning especially after traffic spikes or when a new feature dramatically increases prompt size.
Symptoms
- Intermittent 429 responses from the OpenAI API, often clustering during peak traffic windows
- openai.RateLimitError thrown in logs with message 'Rate limit reached for gpt-4o on tokens per min'
- Requests succeeding for the first 30–60 seconds then failing in bursts
- Batch processing jobs completing the first N items then stalling entirely
- Different error rates on different server instances sharing the same API key
- Successful retries after a 20–60 second pause suggesting TPM reset cycle
- High latency spikes immediately before the 429 cascade starts
- Embedding pipelines failing mid-batch when tokenizing large documents
Business Impact
Rate limit failures translate directly into broken user-facing features. Chat completions that never resolve, document summaries that silently return empty strings, and search results that degrade to keyword matching while the embedding pipeline is down all of these erode user trust and generate support tickets. For B2B products with SLA commitments, sustained 429 cascades during business hours can trigger contractual penalties.
Root Cause
OpenAI rate limits are applied per API key per model per minute. The TPM limit is often the first to blow not RPM because developers underestimate how quickly tokens accumulate when system prompts are large, conversation history is included verbatim, or document chunks are sent without truncation. At the Tier 1 level gpt-4o allows 30,000 TPM. A single request with a 10,000-token system prompt plus a 5,000-token document leaves only 15,000 TPM for the rest of your traffic.
TPM limits reset every 60 seconds from your first request in that window, not on the clock minute. Sleeping for 60 seconds after a 429 is not always enough check the Retry-After header in the response for the exact wait time.
Diagnosis
Step 1: Read the 429 Response Headers
The OpenAI API returns informative headers on every 429 response. Log the full error object including headers to understand whether you are hitting RPM or TPM limits.
import OpenAI from "openai";
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
async function diagnoseRateLimit() {
try {
const response = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: "Hello" }],
});
console.log("Success:", response.usage);
} catch (err) {
if (err instanceof OpenAI.RateLimitError) {
console.error("Rate limit hit:", {
status: err.status, // 429
message: err.message, // "Rate limit reached for tokens per min..."
headers: err.headers, // { "retry-after": "20", "x-ratelimit-limit-tokens": "30000" }
type: err.type, // "requests" | "tokens"
});
}
throw err;
}
}Step 2: Count Tokens Before Sending
Install tiktoken to measure token consumption before the request leaves your server. This lets you gate requests that would blow your per-minute budget.
import { encoding_for_model } from "tiktoken";
export function countTokens(model: string, messages: { role: string; content: string }[]): number {
const enc = encoding_for_model(model as any);
let total = 0;
for (const msg of messages) {
// Each message adds 4 tokens for formatting overhead
total += 4;
total += enc.encode(msg.role).length;
total += enc.encode(msg.content).length;
}
// Every reply is primed with 3 tokens
total += 3;
enc.free();
return total;
}
// Usage
const tokens = countTokens("gpt-4o", [
{ role: "system", content: systemPrompt },
{ role: "user", content: userMessage },
]);
console.log(`Request will consume ~${tokens} tokens`);Step 3: Implement Exponential Backoff with Jitter
Naive retry logic retries immediately, which floods the API with the same requests that just failed. Exponential backoff with full jitter spreads retries across time and respects the Retry-After header.
import OpenAI from "openai";
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
async function withBackoff<T>(
fn: () => Promise<T>,
maxRetries = 5,
baseDelayMs = 1000
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err) {
if (!(err instanceof OpenAI.RateLimitError) || attempt === maxRetries) {
throw err;
}
// Respect Retry-After header if present
const retryAfter = err.headers?.["retry-after"];
const retryAfterMs = retryAfter ? parseInt(retryAfter, 10) * 1000 : null;
// Exponential backoff with full jitter: random(0, base * 2^attempt)
const exponential = baseDelayMs * Math.pow(2, attempt);
const jitter = Math.random() * exponential;
const delay = retryAfterMs ?? jitter;
console.warn(
`RateLimitError on attempt ${attempt + 1}/${maxRetries + 1}. Retrying in ${Math.round(delay)}ms...`
);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw new Error("Unreachable");
}
// Usage
const completion = await withBackoff(() =>
client.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: "Summarize this document." }],
})
);Step 4: Add a Token Budget Queue with BullMQ
For batch workloads, a simple retry loop is not enough. Use a queue with rate limiter support to cap throughput at the API tier limit.
import { Queue, Worker, RateLimiterOptions } from "bullmq";
import OpenAI from "openai";
import IORedis from "ioredis";
const connection = new IORedis();
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// Tier 1 gpt-4o limits: 500 RPM, 30,000 TPM
// Being conservative: cap at 400 RPM = ~6-7 per second
const rateLimiter: RateLimiterOptions = {
max: 6, // max jobs per duration
duration: 1000, // 1 second window
};
export const aiQueue = new Queue("openai-completions", {
connection,
defaultJobOptions: {
attempts: 5,
backoff: { type: "exponential", delay: 2000 },
},
});
const worker = new Worker(
"openai-completions",
async (job) => {
const { messages, model = "gpt-4o-mini" } = job.data;
const result = await client.chat.completions.create({ model, messages });
return result.choices[0].message.content;
},
{ connection, limiter: rateLimiter }
);
worker.on("failed", (job, err) => {
console.error(`Job ${job?.id} failed:`, err.message);
});Step 5: Check Your Tier Limits in the OpenAI Dashboard
Navigate to platform.openai.com > Settings > Limits to see your current RPM, TPM, and daily token limits per model. Tier upgrades happen automatically when you cross spending thresholds ($5, $50, $100, $500) confirm you are on the expected tier and request a manual limit increase if needed.
curl https://api.openai.com/v1/models -H "Authorization: Bearer $OPENAI_API_KEY" | jq '.data[].id' | grep gpt-4Production Failure Scenarios
Scenario 1: Thundering Herd After Server Restart
When a server restarts after a deploy, all queued requests fire simultaneously. The burst saturates both RPM and TPM in the first 5 seconds of the minute window, causing a cascade of 429 errors for the next 55 seconds. Fix: add startup jitter randomize the initial delay before worker threads begin processing jobs.
Scenario 2: Shared API Key Across Microservices
Three services share one API key. Service A (chat) uses 20k TPM, Service B (embeddings) uses 8k TPM, and Service C (batch summaries) spikes to 15k TPM at night. The combined 43k TPM exceeds the 30k limit. Fix: either use separate API keys per service (separate billing sub-accounts), or centralize all OpenAI calls through a single API gateway that enforces a shared token budget.
Scenario 3: Embedding Pipeline Using gpt-4o for Embeddings
A developer confused completion models with embedding models and sent chunks to chat completions instead of text-embedding-3-small. Each chunk consumed 10–50x more tokens than necessary. Fix: use text-embedding-3-small or text-embedding-3-large for all embedding workloads they have separate, often larger TPM limits and cost 96% less per token.
Scenario 4: Context Window Growing Unbounded in Chat
A chat feature appended every user and assistant message to the history array indefinitely. After 30 turns the history alone consumed 25,000 tokens, leaving almost no room before hitting TPM. Fix: implement a sliding window keep only the last N turns, or summarize older turns with a cheap gpt-4o-mini call.
Prevention Checklist
- Count tokens with tiktoken before every API call and log the result
- Implement exponential backoff with jitter on all OpenAI API calls
- Respect the Retry-After header parse it from error.headers['retry-after']
- Use separate API keys per service to isolate rate limit consumption
- Route batch jobs through a BullMQ queue with a rate limiter matching your tier
- Cap conversation history with a sliding window or summarization strategy
- Use gpt-4o-mini instead of gpt-4o for tasks that do not require frontier capability
- Use text-embedding-3-small for all embedding operations
- Set up OpenAI usage alerts in the dashboard to trigger at 70% and 90% of daily spend
- Monitor x-ratelimit-remaining-tokens and x-ratelimit-remaining-requests response headers in production
Resolution Summary
Resolving OpenAI rate limit errors requires changes at three layers: pre-request token counting to avoid submitting oversized payloads, request-level backoff logic that respects the Retry-After header, and infrastructure-level queuing that caps throughput to your tier's RPM and TPM limits. Once those three layers are in place, isolated 429 errors become rare and recoverable rather than cascade failures.
Lessons Learned
- TPM limits are almost always the binding constraint, not RPM count tokens first
- Exponential backoff without jitter causes retry storms that make the problem worse
- Sharing an API key across microservices creates invisible resource contention
- The Retry-After header gives you the exact wait time always read it instead of guessing
- Tier upgrades are automatic with spend thresholds but manual limit increases are available via support
- BullMQ rate limiters are the most reliable mechanism for sustained high-throughput pipelines
Conclusion
Rate limit errors are not a sign that you are doing something wrong they are a normal part of operating at scale with metered APIs. The difference between a system that handles them gracefully and one that falls over is a combination of proactive token budgeting, robust retry logic, and queue-based throughput control. Invest in these patterns early and they will save you from emergency incidents during your highest-traffic moments.