DEV Community

Jordan Huang
Jordan Huang

Posted on

429 Is Not a Ban: Six Rate-Limit Myths I Had to Unlearn

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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,
    )
Enter fullscreen mode Exit fullscreen mode

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)