Development Choices

Retry and Backoff for Agent-Driven MCP Calls

Author
Joseph TrasattiMember of technical staff
Published
Section
MCP
Length
7 min read4 sources cited

Retry agent-driven MCP calls in the transport layer, not in the model's reasoning. Retry only 429 and 5xx responses, use exponential backoff with full jitter, cap attempts per account rather than per worker, and make uploads idempotent before retrying them, because a repeated upload that already succeeded creates a duplicate asset instead of an error.

Before you start

You need three things in place before any retry logic is worth writing.

Steps

  1. Classify every failure as retryable or terminal before writing a single retry.

    The distinction is mechanical and it decides everything downstream. A 429 means the account is over its allowance and the same call will succeed once the window moves. A 5xx means the server or the network failed and the same call may succeed on the next attempt. A 400 means the request itself is wrong — bad parameters, malformed identifier, unsupported option — and repeating it produces the identical 400 every time until the request changes. A 401 or 403 is the same: credentials do not become valid by waiting.

    Write the classifier as a function that takes a status code (or an MCP error object carrying one) and returns one of three outcomes: retry, fail now, or fail now and flag as a bug. Anything you cannot classify goes into the terminal bucket. Retrying an unknown error is how a bad request turns into a bad request repeated forty times against a shared allowance. If a call is failing and you are not sure which bucket it belongs in, debugging a failing MCP tool call walks through reading the actual response rather than guessing.

  2. Move the retry into the transport and take it away from the model.

    It is tempting to write “if the tool fails, try again” into the agent’s instructions. Do not. A model asked to retry decides case by case: sometimes it retries immediately, sometimes it gives up on the first error, sometimes it retries a 400 five times with slightly different arguments, sometimes it declares success without checking. Each of those is defensible in isolation and together they produce behaviour you cannot predict, log, or tune. The retry policy is a fixed rule about HTTP semantics, and fixed rules belong in code.

    Concretely: wrap the client so that a tool call which returns a retryable status is repeated by the wrapper and the model only ever sees the final outcome. The model should see a 429 only if the transport has already exhausted its attempts, at which point the correct response is to stop, not to try harder. This also makes the behaviour observable — one place to log attempts, one place to count them — which is what seeing what an agent actually did through an MCP server depends on.

  3. Add exponential backoff with full jitter, not a fixed delay.

    Naive retries wait a fixed interval — say one second — and try again. When one worker does this it is harmless. When twenty do it, every worker that hit the limit at the same instant retries at the same instant one second later, and the burst that caused the 429 is reproduced exactly, with the same result, indefinitely. The workers have synchronised.

    The fix has two parts. Exponential growth — double the wait on each failure — spreads attempts out over time. Jitter — randomising each wait between zero and the current cap — spreads them out across workers, so that no two are likely to land on the same instant. Amazon’s Builders’ Library write-up on timeouts, retries and backoff with jitter is the standard reference; its simulations show that “full jitter” (sleep = random(0, min(cap, base × 2^attempt))) completes the same work with far fewer total calls than either fixed delay or exponential backoff without jitter, and that the difference grows with the number of competing clients. Use full jitter. Do not use “equal jitter” or a small random fudge on top of a fixed delay; the point is that the distribution is wide, not that it is nonzero.

    Cap the maximum wait (thirty to sixty seconds is a common ceiling) and cap the attempt count. Where the response carries a reset time or a Retry-After, honour it as a floor on the next wait; the Cloudinary Admin API reference documents what its responses report. A retry that fires before the window resets is a guaranteed second 429.

  4. Budget retries per account, not per worker.

    Rate limits are enforced on the account. If five agents run in parallel against one product environment, they draw down one allowance, and when one of them starts retrying it is spending the others’ budget. Five workers each configured with “five retries” is not five retries; it is up to twenty-five extra calls against a limit that was already exceeded, each making the next worker’s 429 more likely.

    The practical rule: decide the total concurrent call rate the account can sustain, then divide it among the workers, and treat retries as calls that count against that share. A shared token bucket or semaphore in front of the transport does this directly. If the workers cannot share state — they run on separate machines in CI, for instance — the fallback is to size each worker’s concurrency at (allowance ÷ worker count) with headroom for retries. Running MCP-driven work in CI covers the case where the worker count is not fixed. And if the retry policy still trips the limit under normal load, the answer is fewer parallel workers, not more patient retries; agent execution under provider rate limits and concurrency caps covers that trade-off.

  5. Make uploads idempotent before you let them retry.

    Reads are safe to repeat: a listing or a resource fetch that succeeded but whose response was lost in transit can be re-run and returns the same answer. Uploads are not. If an upload succeeded but the client timed out before reading the response, a retry sends the asset again, the server accepts it again, and you now have two assets — no error, no 409, just a duplicate that costs storage and shows up in every subsequent listing. Over a long agent run, this compounds quietly.

    Before allowing retries on any write, give each upload an identifier that you choose and that the server treats as the identity of the asset, so a second attempt overwrites or is rejected rather than duplicated. If the server does not offer that guarantee for a given operation, the transport must check for the asset’s existence by that identifier before resending, and treat “already there” as success. Which tools on the Asset Management MCP server are reads and which are writes is worth listing explicitly so the wrapper applies the stricter rule to the right calls. Deletes are idempotent in the other direction — deleting something already gone should be treated as success, not a terminal error.

Expected result

When this is done, the model never sees a transient failure. A 429 or 5xx is absorbed by the transport, retried with exponentially growing, randomly jittered waits, and surfaces to the model only after the attempt cap is exhausted, at which point the run stops rather than pressing on. A 400, 401 or 403 surfaces immediately, once, with the original response body attached. Parallel workers share a call budget and their retries do not synchronise into repeat bursts. Every write carries a caller-chosen identifier, so a retried upload lands on the same asset and a retried delete is a no-op. Retry counts, waits and outcomes are logged in one place, so a spike in 429s reads as a concurrency problem to fix and not as noise to retry through.

Sources

  1. Cloudinary pricingcloudinary.com
  2. Admin API rate-limits sectioncloudinary.com
  3. timeouts, retries and backoff with jitteraws.amazon.com
  4. Admin API referencecloudinary.com

See also