Claude API Rate Limits — Designing Automation That Doesn't Stall
When you run several blogs on automated posting, you notice a strange thing: requests stall only in the moments when a batch goes out at night. Chase it down and the cause is almost never a bug in the code — it's rate limiting. If you're folding the Claude API into your own automation as an indie developer, the more you understand this behavior up front, the less you scramble later.
Here I walk through how RPM, TPM, and TPD work, how to save tokens, how to control request flow, and how to retry gracefully when you do hit a limit — in the order I picked them up while running things in production.
How Rate Limits Work
Rate limits ensure API stability and fair resource distribution across all users.
Types of Limits
Claude API applies rate limits on three axes:
| Limit Type | Description | Unit |
|---|---|---|
| RPM (Requests Per Minute) | Maximum requests per minute | Request count |
| TPM (Tokens Per Minute) | Maximum input + output tokens per minute | Token count |
| TPD (Tokens Per Day) | Maximum input + output tokens per day | Token count |
Plan-Based Limits
Rate limits vary by plan and model. Check Anthropic's documentation for the latest values. Higher-tier plans generally receive higher limits.
Using Response Headers
API responses include headers showing your current rate limit status. Use these to control request flow:
RateLimit-Limit-Requests: 50
RateLimit-Limit-Tokens: 90000
RateLimit-Remaining-Requests: 35
RateLimit-Remaining-Tokens: 72000
RateLimit-Reset-Requests: 2026-03-09T12:00:30Z
RateLimit-Reset-Tokens: 2026-03-09T12:00:30Z
Retry-After: 15
Optimizing Token Usage
Optimizing token consumption improves both cost efficiency and rate limit headroom.
1. Prompt Optimization
Write concise, effective prompts to reduce input tokens:
# Inefficient prompt (high token count)
inefficient_prompt = """
Please read the following text. Please analyze the content of the text.
Based on your analysis, please generate a summary of the text.
Please make the summary concise. Keep it as short as possible.
Avoid long summaries. Extract only the key points.
"""
# Optimized prompt (same intent, fewer tokens)
efficient_prompt = """
Summarize the following text in 3 sentences or fewer.
"""2. Setting max_tokens Appropriately
Set max_tokens to the minimum needed for your use case:
import anthropic
client = anthropic.Anthropic()
# For short expected responses
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=256, # Use a small value for short responses
messages=[
{"role": "user", "content": "Translate this word to Japanese: efficiency"}
]
)3. Efficient System Prompts
System prompts are sent with every request, so brevity matters:
# Verbose system prompt
verbose_system = """
You are an excellent assistant. Please answer user questions politely.
Answers should be accurate, easy to understand, and of appropriate length.
Include examples when needed. If uncertain, communicate that clearly.
Understand the user's intent precisely and provide optimal answers.
"""
# Optimized system prompt
concise_system = "Accurate, concise technical assistant. State uncertainty when present."4. Pre-Checking Token Counts
Estimate token counts before sending requests to avoid rate limit overruns:
import anthropic
client = anthropic.Anthropic()
# Use the token counting API
count_response = client.messages.count_tokens(
model="claude-sonnet-4-6",
messages=[
{"role": "user", "content": "A long text..."}
],
system="System prompt"
)
print(f"Input tokens: {count_response.input_tokens}")Request Efficiency Techniques
1. Request Queuing
Queue requests with flow control to prevent hitting rate limits:
import asyncio
import time
from collections import deque
class RequestQueue:
"""Rate-limit-aware request queue"""
def __init__(self, max_rpm: int = 50, max_tpm: int = 90000):
self.max_rpm = max_rpm
self.max_tpm = max_tpm
self.request_times: deque = deque()
self.token_usage: deque = deque()
self.lock = asyncio.Lock()
async def wait_if_needed(self, estimated_tokens: int):
"""Wait if approaching rate limits"""
async with self.lock:
now = time.time()
# Remove entries older than 1 minute
while self.request_times and now - self.request_times[0] > 60:
self.request_times.popleft()
while self.token_usage and now - self.token_usage[0][0] > 60:
self.token_usage.popleft()
# RPM check
if len(self.request_times) >= self.max_rpm:
wait_time = 60 - (now - self.request_times[0])
if wait_time > 0:
print(f"RPM limit approaching. Waiting {wait_time:.1f}s")
await asyncio.sleep(wait_time)
# TPM check
current_tokens = sum(t[1] for t in self.token_usage)
if current_tokens + estimated_tokens > self.max_tpm:
wait_time = 60 - (now - self.token_usage[0][0])
if wait_time > 0:
print(f"TPM limit approaching. Waiting {wait_time:.1f}s")
await asyncio.sleep(wait_time)
self.request_times.append(time.time())
self.token_usage.append((time.time(), estimated_tokens))2. Using Batch Processing
For high-volume workloads, the Message Batches API helps avoid rate limit pressure:
import anthropic
client = anthropic.Anthropic()
# Create a batch request
batch = client.messages.batches.create(
requests=[
{
"custom_id": f"request-{i}",
"params": {
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"messages": [
{"role": "user", "content": f"Task {i}: {task}"}
]
}
}
for i, task in enumerate(tasks)
]
)
# Check batch status
result = client.messages.batches.retrieve(batch.id)
print(f"Status: {result.processing_status}")Key benefits of batch processing:
- Rate limits are managed separately from real-time requests
- 50% cost discount
- Efficient handling of large request volumes
- Results delivered within 24 hours
3. Leveraging Prompt Caching
Use caching for repeated prompts to reduce token reprocessing:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=[
{
"type": "text",
"text": "Large context content...",
"cache_control": {"type": "ephemeral"}
}
],
messages=[
{"role": "user", "content": "Your question"}
]
)Cache hits reduce input token costs by 90%.
When You Hit a Limit — Don't Ignore Retry-After
No matter how well you smooth the flow, on a shared platform the chance of hitting a limit never quite reaches zero. I once found my auto-posting script stalled on a 429 in the middle of the night and only noticed the next morning. What that taught me: rather than trying to avoid the error, it's sturdier to design what happens after you receive a 429.
When the Claude API returns a 429, it often tells you how long to wait via the Retry-After header. Honoring that value beats guessing a delay yourself. Only when it's absent should you back off exponentially — and add a little jitter so retries from multiple processes don't collide on the same instant.
import time, random
import anthropic
from anthropic import RateLimitError
client = anthropic.Anthropic()
def create_resilient(messages, max_retries=6):
"""Honor Retry-After on 429; otherwise back off with jitter."""
backoff = 2.0
for attempt in range(max_retries):
try:
return client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=messages,
)
except RateLimitError as e:
if attempt == max_retries - 1:
raise
retry_after = None
resp = getattr(e, "response", None)
if resp is not None:
hdr = resp.headers.get("retry-after")
if hdr:
retry_after = float(hdr)
wait = retry_after if retry_after is not None else backoff
wait += random.uniform(0, 0.5) # jitter spreads out concurrent retries
time.sleep(wait)
backoff *= 2
raise RuntimeError("max retries exceeded")The priority that matters here: follow the server when it tells you how long to wait, and decide for yourself only when it doesn't. A fixed delay falls short during congestion and wastes time when things are quiet. Since I moved Dolice's automation to this shape, the nightly jobs almost never die on an error.
Monitoring and Visibility
Usage Tracking
import anthropic
from datetime import datetime
class UsageTracker:
"""Track API usage metrics"""
def __init__(self):
self.requests = []
def track(self, response):
"""Record usage from a response"""
self.requests.append({
"timestamp": datetime.now().isoformat(),
"model": response.model,
"input_tokens": response.usage.input_tokens,
"output_tokens": response.usage.output_tokens,
"cache_read_tokens": getattr(
response.usage, "cache_read_input_tokens", 0
),
"cache_creation_tokens": getattr(
response.usage, "cache_creation_input_tokens", 0
),
})
def summary(self):
"""Print usage summary"""
total_input = sum(r["input_tokens"] for r in self.requests)
total_output = sum(r["output_tokens"] for r in self.requests)
total_cache = sum(r["cache_read_tokens"] for r in self.requests)
print(f"Total requests: {len(self.requests)}")
print(f"Total input tokens: {total_input:,}")
print(f"Total output tokens: {total_output:,}")
print(f"Cache read tokens: {total_cache:,}")
# Usage
tracker = UsageTracker()
response = client.messages.create(...)
tracker.track(response)
tracker.summary()Scaling Strategies
Gradual Scale-Up
For production deployments, increase traffic gradually:
- Development: Default rate limits are sufficient
- Testing: Consider plan upgrades if load tests hit limits
- Early production: Monitor real traffic and adjust
- Scale phase: Contact Anthropic for custom rate limits
Requesting Higher Limits
If default limits are insufficient, request increases through the Anthropic Console. Prepare the following information:
- Current usage patterns (actual RPM, TPM values)
- Required limits with justification
- Use case description
- Expected traffic growth rate
Where to Start
Rate limits look like an obstacle, but they're really traffic control that keeps a shared resource fair. Before reaching for a higher plan, there's something you can do first.
The step I'd recommend: add one piece of usage logging to the code you already run. Just recording each response's usage reveals whether your bottleneck is input tokens or request frequency. Once you know which axis is jammed, the fix picks itself — trim prompts, throttle with a queue, or move to batches. Measure, then move. It looks like a detour, but it's the fastest road I've found.