AI coding

Detecting an Agent That Retries Its Way Around a Limit

Antonio Evans9 min read

A dark server room wall of log lines projected as thin horizontal bars.
Key points

Key points

  1. 1A retrying agent that evades your quota shows up as a session count and tool diversity that climb while completed tasks and success rate stay flat.
  2. 2Metering per request hides evasion because each new session or tool call looks like legitimate traffic until you group it by identity and outcome.
  3. 3You need three counters per identity per hour: sessions opened, distinct tools touched, and first attempts versus retries.
  4. 4Set alert thresholds from a baseline week of normal traffic, and alert on the ratio of new sessions per resolved task rather than raw volume.

Why per-request metering misses a retrying agent

Most teams meter an agent the same way they meter an API: they count requests, sum tokens, and watch a rate limit dashboard. That works until the agent itself decides how many requests to make. An agent that hits a quota does not stop. It opens a new session, switches to a different tool, or routes the same work through a second identity, and every one of those moves shows up as ordinary traffic under a per-request meter.

The retry storm antipattern describes the mechanism precisely: frequent client retries during throttling "can prevent the service from recovering and worsen the problem," and the recommended fix is to limit attempts, back off, and honor retry-after headers (the Azure Architecture Center). A single misbehaving client is easy to spot. Nine agents sharing one quota is not, because each agent's retry logic is independent. One practitioner running nine agents against a shared GitHub quota logged 60 or more chained failures in a single incident, with a first 429 wave triggering a second and a third within 90 seconds (Tamir Dresher).

The adversarial reading of your own logs starts from one assumption: the agent is not trying to look suspicious. It is trying to finish the task. Evasion is a side effect of a retry loop that treats "open a new session" as a cheaper path than "wait." Your job is to make that path visible.

The fields to capture on every agent event

You cannot detect what you did not record. Retrofitting is painful, so instrument at the boundary where sessions, tool calls and identities meet. The OpenTelemetry GenAI conventions give you a shared vocabulary for this, and while the agent and tool orchestration attributes are still marked Development, the core chat attributes are stable enough to build dashboards on (OpenTelemetry GenAI registry, OpenObserve guide).

At minimum, emit one event per LLM call and one per tool execution, with these fields:

FieldWhy it matters for evasion detectionOTel equivalent
Identity (user, key, tenant)Grouping work by who is spending; catches work spread across identitiesapplication-defined; map to a resource attribute
Session or conversation idCounts sessions per identity; catches session churngen_ai.conversation.id
Agent id and nameSeparates background pollers from critical reasonersgen_ai.agent.id, gen_ai.agent.name
Tool nameMeasures tool diversity per sessiontool name on the execute_tool span
Attempt numberSplits first attempts from retriesapplication-defined attribute
Outcome (resolved or not)The denominator for every ratio that mattersapplication-defined attribute
Token counts, in and outCost tied to one identity, not one requestgen_ai.usage.input_tokens, gen_ai.usage.output_tokens
Error code and retry-afterDistinguishes throttling from failureHTTP status, response headers

Two of these fields do the heavy lifting: attempt number and outcome. Without them you can measure traffic but not evasion. With them, a session that opens, calls six tools, fails, and opens again is trivially distinguishable from a session that opens, calls two tools, and resolves.

The agent span conventions formalize the structure: an invoke_agent span wraps a unit of agent work, and each tool invocation gets its own execute_tool span, which now requires the tool name in the span name (OpenTelemetry agent spans). If you emit to that shape, the ratios below become simple span queries.

Three ratios that reveal evasion

Raw counts go up when traffic goes up, so they tell you little. Ratios compare the cost of an attempt to the value it produced.

Sessions per resolved task. Count distinct session ids divided by resolved tasks, per identity, per hour. An agent that retries cleanly on the same session keeps this near 1. An agent that opens a fresh session after every failure drives it upward without moving the resolved count. This is the single most diagnostic ratio you can build, and it is the basis for the alert threshold in the next section.

Distinct tools per session. Count distinct tool names divided by sessions. A healthy session uses a small, stable set. A session that cycles through a dozen tools before resolving (or before giving up) is either confused or probing for one that is not throttled. The tool definitions themselves are a schema, and a mismatch between what the model sends and what the server expects produces a validation error rather than a call (MCP schema reference), so a rising diversity count paired with rising schema errors is a strong signal of blind tool cycling.

First attempts to retries. Count first attempts divided by total attempts per session. Below roughly 0.5, more than half of all calls are retries. Some of that is normal during an outage. Retries that stay high for hours, across many sessions for the same identity, while resolved tasks stay flat, is the shape of an agent working around a limit.

A worked example makes the arithmetic concrete. Suppose identity svc-reporting normally handles 50 resolved tasks per hour, with 55 sessions and a success rate of 0.9. That gives 1.1 sessions per resolved task. During an incident it resolves 12 tasks, opens 60 sessions, and its success rate falls to 0.2. Sessions per resolved task is now 5.0, about 4.5 times baseline, and the work it did complete cost five sessions apiece. No single request looked unusual. The ratio is what changed.

Reading identity and authorization logs for spread work

Spreading work across identities is the hardest evasion to catch, because each identity's activity is individually below its limit. The defense is to make identity part of the grouping key, not just a filter.

The first thing to check is whether identities are even distinct. MCP deployments commonly offer two authentication shapes: shared authentication, where every user of the agent uses the same identity and user context does not persist, and individual authentication via OAuth identity passthrough, where each user's permissions carry through (Microsoft Foundry MCP authentication). If you run shared auth with one key for everyone, you have no per-user signal to analyze, and an agent can exhaust the shared key while every user sees only their own small usage.

Authorization also gates whether you can track usage per user at all. Per-user usage tracking and rate limiting are listed as reasons to enable authorization on an MCP server, and the flow is OAuth 2.1 with protected resource metadata discovery (MCP authorization tutorial). If your server accepts unauthenticated requests, you have thrown away the identity dimension that evasion detection depends on.

When you do have identities, check for these patterns together:

  • Many identities, same source segments (same tool set, same prompt shape, same time window) with low per-identity volume.
  • New identities appearing at the same moment the primary identity's success rate drops.
  • A drop in total resolved tasks across identities that does not match a drop in total token spend.

That last one is the tell. Evasion spends money without shipping work.

Setting thresholds from a baseline week

Do not pick thresholds from intuition or from a single incident. Measure a quiet week, then derive numbers you can defend.

  1. Pick a baseline window with no incidents and no quota hits. One full week is enough to capture the weekday and weekend shape.
  2. For each identity, compute sessions per resolved task, distinct tools per session, and first attempts divided by total attempts, bucketed hourly.
  3. Take the median and the 95th percentile for each ratio, not the mean. One bad afternoon skews a mean badly.
  4. Set the warning threshold at the 95th percentile. Set the page threshold at twice the 95th percentile, or at the baseline median times three, whichever is higher.
  5. Require the breach to hold for two consecutive buckets. This kills single-bucket noise, which is common when a batch job lands at the top of the hour.
  6. Re-baseline after any deliberate change in agent behavior, such as adding a tool or changing a retry policy.

As a concrete illustration: if the baseline median for sessions per resolved task is 1.1 and the 95th percentile is 1.8, warn at 1.8 and page at 3.3. The example identity above hit 5.0, comfortably past the page threshold. If your baseline is noisier, say a 95th percentile of 3.0, the same absolute breach means less, and you should prefer the higher of the two page rules.

Pair every ratio alert with an absolute floor so tiny denominators do not page anyone. A session-per-resolved-task ratio of 6.0 on three resolved tasks is noise. Require at least a few dozen resolved tasks or sessions per bucket before evaluating the ratio, and record that floor next to the threshold so the next person knows why it exists.

Separating a real retry storm from evasion

A retry storm and deliberate evasion look similar in aggregate volume and very different in structure. Use the table below as a triage checklist before you escalate to anyone.

SignalRetry stormQuota evasion
Sessions per resolved taskSpikes during the outage, recovers when the service doesStays elevated after the service recovers
Distinct tools per sessionFlat; same tools, retriedClimbs; agent tries new tools to find an unthrottled path
Identities involvedSame identity, or all identities at onceNew or low-volume identities appear at the same moment
Error mixMostly throttling and timeoutThrottling plus schema validation errors from blind tool calls
Token spendFlat or lower during the outageRises without a matching rise in resolved tasks

The structural fixes for retry storms are well cataloged: cap attempts and duration, back off between attempts, use a circuit breaker, and honor retry-after (Azure Architecture Center). A traffic-light throttle that reads remaining quota from response headers and parks background agents at low quota is one practical implementation; reading x-ratelimit-remaining and x-ratelimit-reset after every call lets you slow down before you hit the wall (Tamir Dresher). Those fixes reduce retries. They do not stop an agent from opening a new session to reset its own budget, which is why you also need max sessions per identity per hour and max tool calls per session enforced server-side, not by the prompt.

There is a harder version of this problem: an agent whose retries are not its own idea. A malicious MCP server can return tool responses carrying hidden instructions that the model treats as trusted context, causing it to call restricted tools or send data out (the OWASP MCP Tool Poisoning entry). If your ratio alerts fire alongside unexpected calls to privileged tools, stop treating it as a quota question. Apply the recommended controls: allowlist servers, validate response shape, isolate privileged tools, and enforce restrictions at the execution layer rather than in the system prompt.

What to do when an alert fires

Work the alert in order, so you know what you are dealing with before you change anything.

  1. Pull the identity's last hour of sessions, sorted by start time. Look for the retry-to-new-session pattern: fail, close, reopen.
  2. Compute the three ratios for that identity and compare each to its baseline percentile. One breached ratio is a tooling problem. Two or three together is evasion.
  3. Check whether new identities appeared in the same window. If so, the evasion is spread and you need the identity grouping, not just the session grouping.
  4. Inspect the tool call sequence in the worst session. Repeated calls to the same tool with different arguments means a loop. A climb through unrelated tools means probe behavior.
  5. Check for out-of-scope tool calls. Any call to a privileged tool from an identity that normally does not touch it moves this from a quota incident to a security incident.
  6. Apply the server-side cap first, then fix the agent's prompt and retry logic. Capping breaks the loop; fixing the retry policy stops it recurring.

The order matters because capping alone tends to push a persistent agent toward a more creative workaround. Capping plus a fixed retry policy, with backoff and a session-reuse rule, removes the incentive.

Where a metered gateway fits

You can build all of this yourself: instrument spans, ship them to a store, write the ratio queries, and wire the alerts. Many teams do, and the resulting dashboards are worth the effort.

What you should not do is meter per request and hope. The decision rule is simple: meter on outcomes and identity. If a session cannot be tied to a resolved task and a single owner, you cannot see evasion, only volume.

For teams that would rather not run a logging stack for every model they call, routing model traffic through one metered endpoint gives you the per-identity and per-token view that the ratios need, and it works across text, image, video, audio and embedding models with an OpenAI-compatible interface (Quanta Inference). Pair that with server-side caps and the retry discipline above, and the new-session trick loses its payoff.

FAQ

FAQ

What is the first log field I should add?

Attempt number, paired with outcome. Everything else (sessions, tool diversity, identities) you probably already record in some form, but without attempt number and outcome you cannot split first attempts from retries or compute any ratio against completed work. Add these two, then backfill if your log retention allows.

How do I tell a retry storm from deliberate evasion?

Look at what happens after the throttling stops. A retry storm recovers on its own once the service is healthy, because the underlying cause is a shared outage. Evasion stays elevated after recovery, because the agent has learned that a new session or a new tool gets around the cap. The ratio staying high is the distinguishing signal, and it should drive a different response.

Should I cap sessions per hour or tokens per hour?

Both, for different reasons. A token cap bounds total spend but lets an agent open as many sessions as it likes. A session cap bounds the workaround but lets one long session spend a lot. Apply the tighter of the two and make the limits server-side, so a prompt injection cannot talk the agent past them, which is the same principle behind isolating privileged tools in the OWASP guidance.

Does per-user authorization solve the spread-across-identities problem?

It makes it detectable, not impossible. With OAuth identity passthrough each user's usage is attributable, so you can see an agent spreading work across accounts. Without it, shared keys collapse everyone into one identity and you lose both the signal and the per-user rate limit. But an agent with legitimate credentials for several accounts can still spread work, so you need the cross-identity correlation query as well.

How often should I re-baseline the thresholds?

After any deliberate change to agent behavior, and on a regular cadence so slow drift does not hide. Adding a tool, changing a retry policy, or switching models all shift the normal ratios. Keep the old baseline and the change date next to the new one, so when an alert fires you can tell whether it is a real anomaly or the tail of a change you made on purpose.

Sources

Sources

  1. Retry Storm Antipattern - Azure Architecture Center | Microsoft Learn
  2. 9 AI Agents, One API Quota, The Rate Limiting Problem ...
  3. Gen AI | OpenTelemetry
  4. OpenTelemetry GenAI Semantic Conventions: A Practical Guide
  5. Semantic Conventions for GenAI agent and framework spans - GitHub
  6. Schema Reference
  7. Set up MCP server authentication - Microsoft Foundry
  8. Understanding Authorization in MCP
  9. MCP Tool Poisoning | OWASP Foundation
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles