n4nAI

How OpenAI rate limits scale with usage tier

Understand how OpenAI rate limits scale across usage tiers, with practical strategies for handling limits, monitoring usage, and designing resilient LLM applications.

n4n Team5 min read1,094 words

Audio narration

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

OpenAI rate limits usage tier determines how many requests and tokens your application can consume per minute. If you’re building production systems on top of the API, you need to understand the tier structure, how limits compound across models, and what happens when you hit the ceiling. This guide walks through the current tier system, shows you how to monitor your position, and gives you concrete patterns for staying within bounds.

The tier structure

OpenAI organizes accounts into five usage tiers. Each tier unlocks higher rate limits and access to more models. Progression is automatic based on cumulative spend and account age — you don’t request upgrades.

Tier Min spend Age requirement RPM (gpt-4o) TPM (gpt-4o)
Free $0 3 40,000
Tier 1 $5 7 days 500 200,000
Tier 2 $50 7 days 5,000 2,000,000
Tier 3 $100 7 days 10,000 4,000,000
Tier 4 $250 14 days 30,000 10,000,000
Tier 5 $1,000 30 days 50,000 30,000,000

These numbers are per-model. Limits for gpt-4o-mini, o1-preview, and other models differ — sometimes significantly. Always check the current limits page for your target model.

The key insight: rate limits scale roughly linearly with spend, but token limits scale faster than request limits. At Tier 1 you get 500 RPM but 200k TPM — that’s 400 tokens per request average. At Tier 5 you get 50k RPM but 30M TPM — 600 tokens per request. If your workloads are token-heavy (long contexts, large outputs), you’ll hit TPM before RPM.

How limits actually work

OpenAI enforces two independent sliding windows: requests per minute (RPM) and tokens per minute (TPM). Both are rolling 60-second windows, not fixed calendar minutes. A burst of 500 requests at 10:00:30 consumes your Tier 1 RPM budget until 10:01:30.

Token counting includes both input and output tokens. If you send 10,000 input tokens and request 5,000 output tokens, that’s 15,000 tokens against your TPM budget — even if the model only generates 1,000 tokens before stopping. The max_tokens parameter reserves capacity upfront.

Headers on every response tell you where you stand:

import openai

client = openai.OpenAI()

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Hello"}],
    max_tokens=100
)

# These headers are on the raw response
print(response.headers.get("x-ratelimit-limit-requests"))      # 500
print(response.headers.get("x-ratelimit-remaining-requests"))  # 499
print(response.headers.get("x-ratelimit-limit-tokens"))        # 200000
print(response.headers.get("x-ratelimit-remaining-tokens"))    # 199900
print(response.headers.get("x-ratelimit-reset-requests"))      # "0.5s"
print(response.headers.get("x-ratelimit-reset-tokens"))        # "0.8s"

The reset headers tell you seconds until the oldest request in the window expires. Use these for precise backoff instead of fixed sleeps.

Monitoring your tier and limits

You can’t query your current tier via the API. You have to check the usage dashboard or infer from the limit headers. A practical pattern: log the limit headers on every request and alert when remaining drops below a threshold.

import logging
import time
from openai import OpenAI, RateLimitError

client = OpenAI()
logger = logging.getLogger(__name__)

RPM_WARN_THRESHOLD = 0.15  # warn at 15% remaining
TPM_WARN_THRESHOLD = 0.15

def chat_with_monitoring(messages, model="gpt-4o", **kwargs):
    response = client.chat.completions.create(
        model=model,
        messages=messages,
        **kwargs
    )
    
    # Parse rate limit headers
    rpm_limit = int(response.headers.get("x-ratelimit-limit-requests", 0))
    rpm_remaining = int(response.headers.get("x-ratelimit-remaining-requests", 0))
    tpm_limit = int(response.headers.get("x-ratelimit-limit-tokens", 0))
    tpm_remaining = int(response.headers.get("x-ratelimit-remaining-tokens", 0))
    
    rpm_pct = rpm_remaining / rpm_limit if rpm_limit else 1.0
    tpm_pct = tpm_remaining / tpm_limit if tpm_limit else 1.0
    
    if rpm_pct < RPM_WARN_THRESHOLD:
        logger.warning(f"RPM low: {rpm_remaining}/{rpm_limit} ({rpm_pct:.1%})")
    if tpm_pct < TPM_WARN_THRESHOLD:
        logger.warning(f"TPM low: {tpm_remaining}/{tpm_limit} ({tpm_pct:.1%})")
    
    return response

For dashboard-level visibility, pull usage data daily via the usage endpoint and correlate with your tier thresholds:

curl -H "Authorization: Bearer $OPENAI_API_KEY" \
  "https://api.openai.com/v1/usage?start_time=$(date -d 'yesterday' +%s)&end_time=$(date +%s)"

Handling rate limit errors

When you exceed a limit, OpenAI returns HTTP 429 with a Retry-After header (seconds). The SDK raises RateLimitError. Don’t retry immediately — respect the header.

from openai import OpenAI, RateLimitError
import time

client = OpenAI()

def chat_with_retry(messages, model="gpt-4o", max_retries=3, **kwargs):
    for attempt in range(max_retries):
        try:
            return client.chat.completions.create(
                model=model,
                messages=messages,
                **kwargs
            )
        except RateLimitError as e:
            if attempt == max_retries - 1:
                raise
            
            # Extract retry-after from response headers
            retry_after = 1.0
            if e.response and e.response.headers:
                retry_after = float(e.response.headers.get("Retry-After", 1.0))
            
            # Add jitter to prevent thundering herd
            sleep_time = retry_after + (0.1 * attempt)
            logger.warning(f"Rate limited, sleeping {sleep_time:.1f}s (attempt {attempt + 1})")
            time.sleep(sleep_time)
    
    raise RuntimeError("Unreachable")

Pitfall: The Retry-After header may be missing on some error paths. Default to exponential backoff with a cap:

def calculate_backoff(attempt, base=1.0, max_backoff=60.0):
    import random
    backoff = min(base * (2 ** attempt), max_backoff)
    return backoff + random.uniform(0, 0.1 * backoff)  # jitter

Designing for tier constraints

Request batching

If you’re on Tier 1-2 and need throughput, batch multiple prompts into single requests using the n parameter or process multiple inputs sequentially within your RPM budget. For embeddings, the batch endpoint handles 2,048 inputs per request — far more efficient than individual calls.

# Instead of 100 separate embedding calls:
embeddings = client.embeddings.create(
    model="text-embedding-3-small",
    input=texts[:2048],  # max batch size
    dimensions=512
)

Token budgeting

Reserve token budget for output. If your TPM limit is 200k and you send 150k input tokens, you only have 50k left for generation — even if your max_tokens is 100k. The API will reject the request if input_tokens + max_tokens > TPM_remaining.

def estimate_token_budget(messages, max_output, model="gpt-4o"):
    import tiktoken
    enc = tiktoken.encoding_for_model(model)
    input_tokens = sum(len(enc.encode(m["content"])) for m in messages)
    return input_tokens + max_output

def can_fit_in_tpm(input_tokens, max_output, tpm_remaining):
    return (input_tokens + max_output) <= tpm_remaining

Priority queues

Not all requests are equal. User-facing chat needs low latency; background summarization can wait. Implement a priority queue that respects your RPM/TPM budget:

import heapq
import threading
import time
from dataclasses import dataclass, field
from typing import Callable, Any

@dataclass(order=True)
class QueuedRequest:
    priority: int  # lower = higher priority
    timestamp: float = field(compare=False)
    func: Callable = field(compare=False)
    args: tuple = field(compare=False)
    kwargs: dict = field(compare=False)
    future: Any = field(compare=False)

class RateLimitedExecutor:
    def __init__(self, rpm_limit: int, tpm_limit: int):
        self.rpm_limit = rpm_limit
        self.tpm_limit = tpm_limit
        self.request_times = []  # timestamps of recent requests
        self.token_usage = []    # (timestamp, tokens) pairs
        self.queue = []
        self.lock = threading.Lock()
        self.worker_thread = threading.Thread(target=self._worker, daemon=True)
        self.worker_thread.start()
    
    def _prune_windows(self, now: float):
        cutoff = now - 60
        self.request_times = [t for t in self.request_times if t > cutoff]
        self.token_usage = [(t, tok) for t, tok in self.token_usage if t > cutoff]
    
    def _current_rpm(self) -> int:
        return len(self.request_times)
    
    def _current_tpm(self) -> int:
        return sum(tok for _, tok in self.token_usage)
    
    def submit(self, priority: int, func: Callable, *args, **kwargs):
        future = threading.Future()
        with self.lock:
            heapq.heappush(self.queue, QueuedRequest(
                priority=priority,
                timestamp=time.time(),
                func=func,
                args=args,
                kwargs=kwargs,
                future=future
            ))
        return future
    
    def _worker(self):
        while True:
            with self.lock:
                self._prune_windows(time.time())
                
                if not self.queue:
                    time.sleep(0.1)
                    continue
                
                # Check if we have budget for the highest priority request
                req = self.queue[0]
                # Estimate tokens (simplified - you'd want real estimation)
                estimated_tokens = 1000
                
                if (self._current_rpm() < self.rpm_limit and 
                    self._current_tpm() + estimated_tokens < self.tpm_limit):
                    heapq.heappop(self.queue)
                else:
                    time.sleep(0.1)
                    continue
            
            # Execute outside the lock
            try:
                result = req.func(*req.args, **req.kwargs)
                req.future.set_result(result)
            except Exception as e:
                req.future.set_exception(e)
            
            # Record usage
            with self.lock:
                now = time.time()
                self.request_times.append(now)
                self.token_usage.append((now, estimated_tokens))

This is a simplified skeleton — production versions need better token estimation, dead letter handling, and metrics.

Multi-model and multi-tier strategies

Different models have different limits. gpt-4o-mini at Tier 1 gets 3,000 RPM / 1,000,000 TPM — much higher than gpt-4o. Route workloads accordingly:

MODEL_LIMITS = {
    "gpt-4o": {"rpm": 500, "tpm": 200_000},
    "gpt-4o-mini": {"rpm": 3_000, "tpm": 1_000_000},
    "o1-preview": {"rpm": 20, "tpm": 50_000},
    "text-embedding-3-small": {"rpm": 3_000, "tpm": 1_000_000},
}

def select_model_for_workload(task_type: str, tier: int) -> str:
    if task_type == "chat" and tier >= 2:
        return "gpt-4o"
    elif task_type == "chat":
        return "gpt-4o-mini"
    elif task_type == "reasoning":
        return "o1-preview"
    elif task_type == "embeddings":
        return "text-embedding-3-small"
    return "gpt-4o-mini"

If you operate multiple OpenAI accounts (common for isolation between environments or clients), you can distribute load across them. Some teams use a gateway that routes requests to the account with the most remaining capacity. n4n.ai handles this automatically — it forwards requests to providers with available capacity and honors routing directives you pass in headers.

Common pitfalls

Assuming fixed-minute windows. The sliding window means a burst at 10:00:59 consumes budget until 10:01:59. Don’t assume limits reset at the top of the minute.

Ignoring max_tokens reservation. Setting max_tokens=4096 for a 100-token response wastes TPM budget. Set it close to your actual expected output.

Not accounting for retry tokens. A failed request that retries consumes budget twice if the first request was processed but the response dropped. Idempotency keys (via openai-client-id header) help but don’t eliminate this.

Treating all models equally. o1-preview has drastically lower limits than gpt-4o-mini. A single o1 request can consume 50k tokens — at Tier 1 that’s 25% of your TPM budget.

Forgetting organization-level limits. If you’re on a shared organization, your tier limits are shared across all projects and API keys. One runaway script can starve everyone.

Checking your current tier programmatically

There’s no API endpoint for tier. But you can infer it from the limit headers on a known model. Make a minimal request to gpt-4o and read x-ratelimit-limit-requests:

def detect_tier(client: OpenAI) -> int:
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": "x"}],
        max_tokens=1
    )
    rpm_limit = int(response.headers.get("x-ratelimit-limit-requests", 0))
    
    tier_map = {
        3: 0,      # Free
        500: 1,    # Tier 1
        5_000: 2,  # Tier 2
        10_000: 3, # Tier 3
        30_000: 4, # Tier 4
        50_000: 5, # Tier 5
    }
    return tier_map.get(rpm_limit, -1)

Run this at startup and log the detected tier. If it’s lower than expected, check for unpaid invoices or organization-level spending caps.

Scaling beyond Tier 5

Tier 5 caps at 50k RPM / 30M TPM for gpt-4o. If you need more, you have three options:

  1. Request a limit increase via the OpenAI support form. They evaluate case by case and require usage justification.
  2. Distribute across multiple organizations — each org gets its own Tier 5 limits.
  3. Use a gateway that load-balances across multiple OpenAI accounts and/or alternative providers (Anthropic, Google, open models).

The gateway approach also gives you resilience against provider outages and model deprecations. It’s the pattern most high-volume teams converge on.

Summary checklist

  • Know your current tier and the RPM/TPM limits for each model you use
  • Log x-ratelimit-remaining-* headers on every request
  • Alert when remaining capacity drops below 15-20%
  • Implement retry logic that respects Retry-After with jitter
  • Budget tokens for max_tokens, not just actual output
  • Route high-volume, low-complexity work to gpt-4o-mini or embeddings models
  • Use priority queues for mixed latency/throughput workloads
  • Monitor organization-level usage, not just per-key
  • Plan your Tier 5+ strategy before you hit the wall

Rate limits aren’t a blockade — they’re a capacity planning signal. Treat them like any other resource constraint: measure, budget, and scale deliberately.

Tagsrate-limitsopenaiapi-quotas

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 rate limits & api quotas posts →