PromptABCD
FeaturesLearnHow it worksUse casesFAQGuideBlogContext Blocks
Sign inGet started free
Sign inSign up
PromptABCD

A calm home for your best AI prompts. Save them once, find them in seconds, reuse them forever.

Product

  • Features
  • Chrome Extension
  • Free Courses
  • How it works
  • Use cases
  • Blog
  • Context Blocks
  • Export Anywhere
  • FAQ

Resources

  • User guide
  • Learn prompting
  • Sign in
  • Get started free

© 2026 PromptABCD. All rights reserved.

Privacy PolicyTerms and Conditions
Home/Blog/AI Harness/Rate Limiting Inside the Agent Harness
AI Harness

Rate Limiting Inside the Agent Harness

Agent harness rate limiting done reactively makes overload worse. Learn proactive, token-aware limiting that stops the 429 retry storm before it starts.

September 1, 2026·9 min read
ShareShare
⚡Featured Prompt— copy and use right now
def call_model(messages, model, tries=5):
    for _ in range(tries):
        try:
            return model.complete(messages)
        except RateLimitError:
            time.sleep(1)      # wait a second, try again
    raise RuntimeError("gave up after rate limits")

Picture this: you're an engineer who just launched an agent to a few hundred users, and your dashboard lights up red. Every request is failing with a 429. Your agent hit the provider's rate limit, and worse, your naive retry logic is now hammering the API with retries that also get rate-limited, so you're making the problem worse with every attempt. A little agent harness rate limiting — done proactively instead of reactively — would have prevented the whole cascade. Let's tear down the "just retry on 429" approach and build a limiter that stops the storm before it starts — one that shapes traffic on the way out instead of scrambling to recover after a rejection.

Before: Reacting to Rate Limits

Here's what most harnesses do about rate limits, which is to say, nothing until one hits:

hljs python
[object Object], ,[object Object],(,[object Object],):
    ,[object Object], _ ,[object Object], ,[object Object],(tries):
        ,[object Object],:
            ,[object Object], model.complete(messages)
        ,[object Object], RateLimitError:
            time.sleep(,[object Object],)      ,[object Object],
    ,[object Object], RuntimeError(,[object Object],)

What this does: it fires requests with no restraint, and only when one comes back rate-limited does it wait a flat second and retry. It's purely reactive — the limiter does nothing until you're already over the limit. Under load, this is exactly the wrong behavior, because the retries pile onto the same overloaded endpoint, and a fixed one-second wait means a whole fleet of agents retries in lockstep.

Why That Approach Fails

Reactive rate limiting has three compounding failures that turn a manageable situation into an outage.

It creates retry storms. When many requests hit the limit at once, they all retry at once, producing a synchronized wave of traffic that hits the limit again. The retries don't relieve the pressure; they add to it. You've built a system that responds to overload by generating more load — the software equivalent of a crowd all pushing toward the same exit and jamming it.

It ignores the two different limits. Providers limit both requests per minute and tokens per minute, and for agents the token limit usually binds first, because agents send large, growing contexts. A limiter that only counts requests sails right past the token limit and gets rate-limited anyway, baffled, because by request count it looked fine.

And it wastes the failed calls. Every 429 is a request that consumed a round trip and returned nothing. At scale, a meaningful fraction of your latency and cost can be requests that were always going to be rejected. Reactive limiting pays full price for calls it should never have sent.

Effective agent harness rate limiting inverts all three of these. Instead of reacting after a rejection, it shapes traffic before sending, so requests leave your machine only when they'll be accepted. Instead of counting one dimension, it tracks both requests and tokens. And instead of wasting round trips on doomed calls, it holds them back until there's real budget. The shift from reactive to proactive is the whole fix, and everything below is how you build it.

⚠️ Common mistake: Rate-limiting on request count alone. For most agents, tokens per minute is the binding constraint, not requests per minute — a single agent step can carry tens of thousands of tokens of accumulated context. A request-only limiter will happily stay under the request cap while blowing through the token cap, then act surprised when the 429s arrive. Track both, and gate on whichever you'll hit first.

After: Proactive Token-Aware Rate Limiting

The fix is a client-side limiter that blocks before sending, tracking both requests and tokens, and respecting the provider's own retry hints. A token bucket does this cleanly:

hljs python
[object Object], time, threading

,[object Object], ,[object Object],:
    ,[object Object], ,[object Object],(,[object Object],):
        ,[object Object],.rpm, ,[object Object],.tpm = rpm, tpm
        ,[object Object],.req_tokens = rpm          ,[object Object],
        ,[object Object],.tok_tokens = tpm          ,[object Object],
        ,[object Object],.updated = time.time()
        ,[object Object],.lock = threading.Lock()

    ,[object Object], ,[object Object],(,[object Object],):
        ,[object Object], ,[object Object],.lock:
            ,[object Object], ,[object Object],:
                now = time.time()
                elapsed = now - ,[object Object],.updated
                ,[object Object],.req_tokens = ,[object Object],(,[object Object],.rpm, ,[object Object],.req_tokens + elapsed * ,[object Object],.rpm / ,[object Object],)
                ,[object Object],.tok_tokens = ,[object Object],(,[object Object],.tpm, ,[object Object],.tok_tokens + elapsed * ,[object Object],.tpm / ,[object Object],)
                ,[object Object],.updated = now
                ,[object Object], ,[object Object],.req_tokens >= ,[object Object], ,[object Object], ,[object Object],.tok_tokens >= est_tokens:
                    ,[object Object],.req_tokens -= ,[object Object],
                    ,[object Object],.tok_tokens -= est_tokens
                    ,[object Object],
                time.sleep(,[object Object],)

What this does: it refills request and token allowances continuously over time, and before any call, it waits until both a request slot and enough token budget are available — then reserves them. Requests are throttled to fit inside both limits proactively, so you rarely hit a 429 at all. The storm never forms because the limiter smooths traffic before it leaves your machine.

The second half is honoring the provider's

Retry-After
header when you do hit a limit:

hljs python
[object Object], ,[object Object],(,[object Object],):
    limiter.acquire(estimate_tokens(messages))
    ,[object Object],:
        ,[object Object], model.complete(messages)
    ,[object Object], RateLimitError ,[object Object], e:
        wait = e.retry_after ,[object Object], ,[object Object],       ,[object Object],
        time.sleep(wait + random.random())
        ,[object Object], call_model(messages, model, limiter)

What this does: it reserves budget from the limiter before sending, and if a limit is somehow still hit, it waits exactly as long as the provider asks — plus jitter — instead of guessing. The provider knows when it'll accept traffic again; the

Retry-After
header is that knowledge, and ignoring it in favor of a fixed sleep is throwing away the one reliable signal you have.

⚡ Pro tip: When a provider doesn't send a

Retry-After
header, back off exponentially with jitter rather than retrying at a fixed interval. A flat retry delay across many workers produces the same synchronized wave the proactive limiter exists to prevent — jittered exponential backoff spreads the retries out so they don't re-collide. Always add the random jitter term; without it, a fleet retries in lockstep and recreates the storm.

Breaking Down the Limiter

Three design points make proactive rate limiting actually work.

Estimate tokens before sending. The limiter needs to know a request's token cost to reserve budget. A rough estimate from the message length is enough — you're gating, not billing. Over-estimate slightly to stay safe.

Sit below the provider's limit. Set your limiter a bit under the real cap — say 90% — to leave headroom for the occasional retry and for estimation error. Running right at the limit means any wobble tips you over.

Share the limiter across concurrent agents. If ten agents run in parallel against one provider account, they share one rate limit, so they must share one limiter. Ten independent limiters each think they have the full budget and collectively blow through it.

Prefer estimation over exactness. You'll never know a request's precise token count before sending it, and that's fine — the limiter's job is to keep you safely under a cap, not to bill accurately. A slightly conservative estimate that runs you at 88% of your real limit costs you a sliver of throughput and buys you near-zero rejections, which is a trade worth making every time. Chasing exact token accounting in the limiter is effort spent in the wrong place; a rough estimate with headroom does the job.

⚡ Pro tip: Make the limiter account-wide, not per-agent. The provider limits your whole account, so a single shared limiter across every agent and worker is the only thing that actually reflects reality. This is the mistake that catches teams scaling from one agent to a fleet — each new worker quietly assumes it owns the full quota.

Variations for Different Contexts

The right limiter setup shifts with scale, and matching it to your situation keeps you from either over-engineering or under-protecting:

  • A solo developer running one agent needs only a simple limiter set just under their tier's limits, mostly to avoid the occasional burst rejection.
  • A platform team running hundreds of concurrent agents needs a shared, distributed limiter — often backed by a central store — so every worker across every machine draws from one budget.
  • A data team running a large batch job proactively paces the whole batch under the token limit, finishing reliably in a predictable time instead of racing to the limit and stalling on retries.

The batch case is worth dwelling on, because it flips the intuition. When you have a thousand tasks to process, the instinct is to fire them all as fast as possible. But against a rate limit, going flat-out means most requests get rejected and retried, and the batch actually finishes slower than if you'd paced it. A proactively paced batch that runs steadily at 90% of the token limit completes in a smooth, predictable window; an unpaced one thrashes against the limit, wastes round trips on rejections, and finishes late with a pile of 429s in the logs. Slower-but-steady beats fast-but-throttled almost every time at the limit.

⚡ Pro tip: Watch your 429 rate as a health metric. With good proactive limiting it should be near zero — an occasional one is fine, a steady stream means your limiter is set too high or isn't shared across workers. A rising 429 rate is an early warning that your traffic shaping has drifted out of step with your real limits.

Save and Reuse This

Good agent harness rate limiting is proactive, not reactive. Gate on both requests and tokens before you send, sit slightly under the provider's cap, honor

Retry-After
when you do hit a limit, and share one limiter across every agent on the account. Each of those four is a distinct failure closed, and together they turn rate limits from a cliff you fall off into a ceiling you glide beneath. The naive retry-on-429 approach doesn't just fail to help — it actively makes overload worse by adding retry traffic to an already-strained endpoint. Proactive shaping does the opposite: it keeps you under the limit so the endpoint never gets strained in the first place.

The limiter configuration and the token-estimation logic you settle on are reusable across every agent you run against a given provider. Keeping those configs and the prompt patterns that keep contexts lean — which directly lowers your token-per-minute pressure — organized in a library like PromptABCD means your next agent starts with rate limiting that reflects your real quotas, tuned once and reused everywhere. Shape the traffic before it leaves, and the red dashboard stays green.

agent harness rate limitingrate limitingtoken bucketai agentsreliability429

Continue Reading

Building a Harness That Swaps Models Easily
AI Harness

Building a Harness That Swaps Models Easily

A model agnostic agent harness turns a week-long provider rewrite into an afternoon. Learn the adapter pattern and why portable code doesn't mean portable behavior.

September 1, 2026·8 min read
Measuring Pass@k for AI Agents (and Why It Misleads)
AI Harness

Measuring Pass@k for AI Agents (and Why It Misleads)

A pass at k agent eval can hide terrible single-attempt reliability. A case study on shipping a 90% pass@5 agent that failed half its first tries in production.

August 31, 2026·8 min read
Golden Datasets for Agent Evaluation, Done Right
AI Harness

Golden Datasets for Agent Evaluation, Done Right

An agent golden dataset is only as good as its governance. Learn to build curated, human-reviewed input-output pairs and review every golden change like code.

August 31, 2026·8 min read

Save the prompts from this post

PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.

Start free →
← PreviousBuilding a Harness That Swaps Models Easily
Share this post:
ShareShare