I used to treat a 429 like a personal failure.
My retry loop hammered the endpoint. The endpoint hammered back. I blamed the server. The server was fine.
AI turned every developer into a reviewer. Nobody taught us how to read a 429. So I learned the hard way, probing free model servers for weeks. That included MonkeyCode's free server option.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Here is the myth-busting FAQ I wish I had read first. Every claim below comes from my own probes. Your numbers will differ. That is the point.
Myth 1: "429 means I'm banned"
A 429 is not a ban. A ban looks like 401 or 403. Those mean authentication or authorization failed.
429 means "too many requests." The server is shedding load. You are one client in a shared queue.
The corrected mental model? Rate limits are load-shedding. They are not judgment.
Free servers share capacity across many users. Their limits feel stricter. That is by design, not a grudge.
Myth 2: "Retry immediately, retry hard"
This was my default. It was also my worst habit.
Immediate retries land in the same rate-limit window. You are not retrying. You are double-spending your quota.
Evidence from my logs: instant-retry bursts produced longer 429 streaks. One backoff ended the streak.
The corrected model is exponential backoff with full jitter.
import random
import time
def call_with_backoff(fn, max_attempts=5, base=0.5, cap=8):
for attempt in range(max_attempts):
response = fn()
if response.status_code != 429:
return response
sleep = random.uniform(0, min(cap, base * (2 ** attempt)))
time.sleep(sleep)
return response
Full jitter randomizes the wait. It prevents thundering herds. Your retries stop syncing up.
Minimal example. Adapt the URL, headers, and payload to your endpoint.
Myth 3: "Rate limits are one number"
Most APIs have two windows. A burst window counts requests per second. A sustained window counts requests per minute or per day.
My probes showed the pattern clearly. Ten requests in one second? 429s. Ten requests spread over ten seconds? Fine.
The corrected model? Test both windows separately. Log both numbers. They tell different stories.
Burst limits protect the shared box. Sustained limits protect the monthly budget. Confusing them caused most of my early 429s.
Here is the probe plan (pseudocode):
# Burst probe: fire N requests as fast as possible
# Sustained probe: fire N requests with 1s spacing
# Compare the status codes. The curves will differ.
Myth 4: "The error message tells me when to retry"
Retry-After is a gift. It is also frequently missing.
Sometimes it holds seconds. Sometimes it holds an HTTP date. Sometimes it holds nothing at all.
The corrected model? Parse defensively. Fall back to your own backoff.
from email.utils import parsedate_to_datetime
from datetime import datetime, timezone
def retry_after_seconds(headers, fallback=2.0):
raw = headers.get("Retry-After")
if not raw:
return fallback
try:
return float(raw)
except ValueError:
try:
target = parsedate_to_datetime(raw)
return max(0.0, (target - datetime.now(timezone.utc)).total_seconds())
except ValueError:
return fallback
Never trust a single format. Your parser should survive both.
Myth 5: "More concurrency means more throughput"
This one hurt to learn.
I cranked concurrency from one to twenty. Success rate dropped. Latency climbed. Throughput went sideways.
The corrected model? Throughput is a curve, not a ladder. Somewhere between one and twenty there is a sweet spot. You have to find it.
Here is the staircase probe I run:
import asyncio
import aiohttp
async def staircase_probe(url, max_concurrency=5, calls_per_step=10):
results = {}
async def fire(session, semaphore):
async with semaphore:
async with session.get(url) as response:
return response.status
async with aiohttp.ClientSession() as session:
for level in range(1, max_concurrency + 1):
semaphore = asyncio.Semaphore(level)
statuses = await asyncio.gather(
*(fire(session, semaphore) for _ in range(calls_per_step))
)
results[level] = statuses.count(200)
return results
Run it against your endpoint. Watch where the 200s start falling. That is your sustainable concurrency.
Myth 6: "Free means no rate limits"
Some developers assume free tiers are unlimited. The opposite is true.
Free servers are the most rate-limited endpoints I probe. They have to be. Capacity is shared. Someone pays for every token.
When I probe MonkeyCode's free server option, I assume strict limits. I am never surprised by a 429. Treat "free" as "shared," not "unlimited."
How I log rate limits
You cannot fix what you do not measure. So I log every rate event.
import logging
def log_rate_event(endpoint, status, headers, elapsed):
retry_after = headers.get("Retry-After", "none")
remaining = headers.get("X-RateLimit-Remaining", "unknown")
logging.info(
"endpoint=%s status=%s retry_after=%s remaining=%s elapsed=%.2f",
endpoint, status, retry_after, remaining, elapsed,
)
Headers vary by provider. Log the ones you see. Patterns emerge fast.
I also log the request ID when the provider sends one. It makes support tickets ten times easier.
The retry decision table
Not every error deserves a retry.
| Status | What it means | What I do |
|---|---|---|
| 429 | Slow down | Backoff with jitter, honor Retry-After |
| 5xx | Server hiccup | Retry with backoff, idempotent calls only |
| 401 / 403 | Auth problem | Do not retry. Fix the key. |
| Other 4xx | Client bug | Do not retry. Fix the code. |
Retrying a 400 is how you burn quota. Retrying a 401 is how you burn time.
Who should not use this approach
This workflow is for probes and batch jobs. It is not a production architecture.
Building a real-time chat app? Backoff is not enough. You need a queue and a circuit breaker.
Running on a paid SLA? The math changes. Your provider's guarantees are different.
Free tiers can change behavior without notice. My evidence comes from my own probes. Your provider, region, and luck will differ.
The corrected mental model
Rate limits are a coordination protocol. They describe the server's current capacity. They are not a score of your worth.
Treat 429 as a signal. Back off. Measure. Repeat.
I stopped watching status codes alone. I started watching the shape of the traffic. That changed everything.
That is the FAQ I needed. Now go read your own logs. Your retry logic is probably lying to you.
Top comments (0)