DEV Community

Sam Chen
Sam Chen

Posted on

Three Retry Anti-Patterns That Quietly Burn a Free Model Quota

Most retries against a free model quota are wasted work. Your agent hits a wall, retries, and burns the same dead budget three more times.

You have probably seen that loop in a log. It looks like activity. It is not progress.

Here is a catalog of three retry anti-patterns. Each entry has symptoms, a root cause, and a replacement pattern you can ship today.

The shift that broke old retry code

AI coding agents are everywhere this month. Free model tiers are part of why.

But a free tier fails differently from a paid API. A paid API fails when it is down. A free tier fails when it is used up.

One state is transient. The other is terminal until a window resets. Your HTTP client cannot tell them apart.

So what should the retry loop actually inspect?

Anti-Pattern 1: Treating every 429 as a slow-down

Symptoms

  • The same step retries three to five times with zero change.
  • Every log line shows an identical 429 and an identical body.
  • The run dies with max retries exceeded several minutes later.

Root cause

The policy was copied from a rate-limited paid API. There, 429 means "slow down". On a free tier, 429 often means "budget exhausted". Retrying a terminal state only delays the failure.

Replacement pattern

Classify the failure before you sleep and retry.

  1. Read Retry-After when the server sends it, and honour it.
  2. Scan the body for quota keywords. Halt on those.
  3. Cap transient retries at three attempts per step.
  4. Add jitter so parallel workers do not thunder.
# Sketch. Not run against any specific provider endpoint.
RETRYABLE = {429, 500, 502, 503, 504}
TERMINAL = {400, 401, 403, 404, 422}
QUOTA_HINTS = ("quota", "exhausted", "credit", "limit reached")

def classify(status: int, body: str) -> str:
    lowered = body.lower()
    if status in TERMINAL:
        return "fail_fast"
    if status == 429 and any(h in lowered for h in QUOTA_HINTS):
        return "halt_and_report"   # no retry, no sleep
    if status == 429:
        return "backoff"           # respect Retry-After
    if status in RETRYABLE:
        return "backoff"
    return "halt_and_report"
Enter fullscreen mode Exit fullscreen mode

The important line is halt_and_report. A terminal state deserves a human decision, not a loop.

Anti-Pattern 2: Counting tokens after the run

Symptoms

  • The agent finishes a long plan, then discovers it ran out mid-way.
  • Nobody can say how much of the budget the run consumed.
  • The failure report arrives after all the useful work is gone.

Root cause

Cost tracking is bolted on as post-hoc reporting. That is accounting, not control. By the time the number appears, the decision window has closed.

Replacement pattern

Keep a ledger that runs ahead of the model.

  1. Estimate the cost of the next step before you send it.
  2. Hold a reserve for the fix pass. Twenty percent is a reasonable start.
  3. Stop at the ceiling and write a checkpoint.
  4. Never let the provider be the first thing to tell you the budget is gone.
from dataclasses import dataclass

@dataclass
class Ledger:
    budget: int             # operator-stated figure; verify your own terms
    spent: int = 0
    reserve: float = 0.20   # room for the repair pass

    def ceiling(self) -> float:
        return self.budget * (1 - self.reserve)

    def can_afford(self, estimate: int) -> bool:
        return self.spent + estimate <= self.ceiling()
Enter fullscreen mode Exit fullscreen mode

Call can_afford before each step. When it returns False, stop cleanly and persist state.

Anti-Pattern 3: Re-running the whole plan after one bad step

Symptoms

  • Step 7 fails, and the run restarts from step 1.
  • Already-verified steps get regenerated with different output.
  • Costs double while confidence drops.

Root cause

The plan is treated as one atomic request. It is not. A plan is a sequence of separately verifiable steps, and only one of them failed.

Replacement pattern

Resume from a fingerprint, not from the top.

  1. Hash each step's inputs into a short fingerprint.
  2. Persist fingerprint -> ok when a step verifies.
  3. Re-emit only steps without a verified fingerprint.
  4. Store partial output hashes so truncation is detectable.
import hashlib, json

def step_fingerprint(step: dict) -> str:
    payload = json.dumps(step, sort_keys=True).encode()
    return hashlib.sha256(payload).hexdigest()[:12]

def pending(plan, checkpoints):
    for step in plan:
        fp = step_fingerprint(step)
        if checkpoints.get(fp) == "ok":
            continue        # verified work is skipped, not repeated
        yield step, fp
Enter fullscreen mode Exit fullscreen mode

Skip verified work. Recompute the failure only.

The decision table I keep next to the loop

Failure Retry? Wait Persist first
429 plus quota keyword No — checkpoint + ledger
429, no quota keyword Yes, max 3 Retry-After, else exp + jitter attempt count
500 / 502 / 503 / 504 Yes, max 5 exp backoff + jitter attempt count
400 / 422 schema error No — payload + prompt hash
Truncated stream Yes immediate partial output hash

Print this table in the run header. It is cheap, and it ends arguments later.

Where a free tier actually belongs

Free model access fits throwaway and exploratory work best. Prototypes, harness tests, and one-off refactors are good candidates.

The MonkeyCode project offers free model access and a free server option. The team states the free allowance is 10 million tokens. Check the current terms yourself before you build anything on a number.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Used that way, the tradeoff is honest. A free server is fine for a harness that runs for minutes. It is not capacity planning, and it is not a substitute for paid guarantees.

Run the ledger before the model, not after. That one habit survives every tier change.

Limitations and who should skip this

  • Teams with hard SLAs should budget for a paid tier. Free quotas reset on someone else's schedule.
  • Long-running batch jobs should not depend on a scratch server.
  • Anyone who cannot verify quota terms should not build billing logic on them.
  • The classifier above is a sketch. Test it against your provider's real error bodies.
  • Quota keywords differ per provider. Copy the hints from actual responses, not from a blog post.

What to do next

Pick one anti-pattern and delete it this week. Start with the classifier; it is roughly ten lines.

Then add the ledger. Then add fingerprints.

If you want a free tier and a free server to run the harness on, the MonkeyCode project page is the entry point. Bring the ledger with you.

Top comments (0)