Skip to content

MCP client should honor Retry-After and retry on 429 instead of failing the tool call #1378

Description

@groksrc

Problem

When the cloud gateway rejects a request with 429, the MCP client surfaces the error to the model and stops. It does not retry, and it discards the Retry-After boundary the server sends specifically so that clients can retry correctly.

A transient, often sub-second rate-limit window therefore becomes a visible task failure. The agent has to notice the error, invent its own backoff, and retry by hand — and many will simply report the failure to the user instead.

Observed in the wild (basic-memory 0.23.2), an agent doing ordinary work hit this on two consecutive calls:

Too many requests. Retry after the current rate-limit window resets.

It reported to the user:

Rate-limited on Basic Memory, so I can't yet distinguish "dangling reference" from "read failed".

...worked around it by falling back to local git inspection, and succeeded on a manual retry ~35 seconds later. Nothing was actually wrong. A single automatic retry would have made the entire episode invisible.

Current behavior

src/basic_memory/mcp/tools/utils.py has five near-identical call wrappers — call_get, call_put, call_patch, call_post, call_query — each containing the same block:

# Client errors: log as info except for 429 (Too Many Requests)
if status_code == 429:
    logger.warning(f"Rate limit exceeded: GET {url}: {error_message}")
...
response.raise_for_status()

and a generic message in get_error_message:

elif status_code == 429:
    return "Too many requests: Please slow down and try again later"

So: one log line, then ToolError. No retry, no backoff, no use of the retry boundary.

What the server already sends

The gateway emits everything a client needs in order to retry precisely:

  • Retry-After (seconds)
  • X-RateLimit-Limit-Tenant, X-RateLimit-Remaining-Tenant, X-RateLimit-Reset-Tenant
  • on the hosted MCP path, a JSON body carrying code: "rate_limit_exceeded", retry_after, and reset_at

All of it is currently dropped.

Proposed fix

  1. Honor Retry-After with bounded automatic retry. On 429, wait the server-provided interval plus a small jitter — jitter matters because concurrent agents sharing a quota will otherwise retry in lockstep and collide again — then retry. Cap both the attempt count and total wait, so a genuinely exhausted long window fails fast instead of hanging a tool call.
  2. Retrying a 429 is safe for every method, including writes. The limiter rejects the request before it is processed, so there is no partial work to duplicate. Worth an explicit comment, since the instinct is to restrict retries to idempotent verbs.
  3. Put the boundary in the message when retries are exhausted. "Too many requests: Please slow down and try again later" gives the model nothing to act on. "Rate limit exceeded. Retry after 43s." lets an agent schedule around it rather than abandoning the task or hot-looping.
  4. Implement once. The five wrappers already share this branch by copy-paste; a shared helper (or an httpx transport / event hook) keeps them from drifting.

Scope

Client-side only. This issue is strictly about the client being a well-behaved consumer of a 429 — honoring a retry boundary the server already publishes.

Whether the underlying quotas are the right shape is a separate question and is deliberately out of scope here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions