PrefectHQ/fastmcp · error · IdentityAssertionError

Server overloaded, please retry

Error message

Server overloaded, please retry

What it means

Raised when the jti replay cache has reached its configured maximum size (jti_cache_max_size), an expired-entry cleanup pass fails to free any space, and the incoming assertion's jti therefore cannot be inserted. The library refuses to grow the cache and rejects the assertion with a backpressure-style 'server overloaded' error rather than accepting it untracked.

Source

Thrown at fastmcp_slim/fastmcp/server/auth/identity_assertion.py:467

        # cache lookup (a 500) instead of a clean invalid_grant.
        jti = claims.get("jti")
        if not jti or not isinstance(jti, str):
            raise IdentityAssertionError("Assertion must include a string jti claim")
        cached_exp = self._jti_cache.get(jti)
        if cached_exp is not None and cached_exp > now:
            raise IdentityAssertionError(f"Assertion replay detected: jti {jti} reused")

        # Enforce the cap BEFORE inserting so a rejected assertion never grows the
        # cache. A fresh jti that would exceed capacity is rejected outright (after
        # a cleanup pass to reclaim any expired entries first).
        if (
            jti not in self._jti_cache
            and len(self._jti_cache) >= self._jti_cache_max_size
        ):
            self._cleanup_expired_jtis()
            if len(self._jti_cache) >= self._jti_cache_max_size:
                logger.warning("ID-JAG jti cache at capacity, possible attack")
                raise IdentityAssertionError("Server overloaded, please retry")
        self._jti_cache[jti] = exp

        logger.debug("ID-JAG validated for subject=%s issuer=%s", sub, iss)
        return claims


def normalize_resource_url(url: str) -> str:
    """Normalize a resource URL by removing query parameters and trailing slashes.

    RFC 8707 allows clients to include query parameters in resource URLs, but
    the server's configured resource URL typically doesn't include them. This
    normalizes both sides for comparison by stripping query and fragment.
    """
    parsed = urlparse(str(url))
    return urlunparse(
        (parsed.scheme, parsed.netloc, parsed.path.rstrip("/"), "", "", "")
    )

View on GitHub (pinned to 1f02114297)

Solutions

  1. Increase the jti cache max size (jti_cache_max_size / settings) to cover peak assertions × TTL
  2. Shorten assertion token lifetime (exp) so jti entries expire and free cache capacity faster
  3. Check whether the volume is an attack and apply rate limiting / WAF rules upstream
  4. Retry with backoff — the error is transient: entries expire and capacity is reclaimed
  5. Monitor the 'ID-JAG jti cache at capacity' warning to size capacity from real traffic

Example fix

// before
IdentityAssertion(jti_cache_max_size=1000)
// after
IdentityAssertion(jti_cache_max_size=100_000)  # sized for peak_rps * ttl_seconds
Defensive patterns

Strategy: retry

Try / catch

try:
    validate(assertion)
except IdentityAssertionError as e:
    if "Server overloaded" in str(e):
        time.sleep(backoff)
        validate(mint_new_assertion())  # retry with backoff, fresh jti
    else:
        raise

Prevention

When it happens

Trigger: More than _jti_cache_max_size distinct, still-unexpired jti values are validated within their TTL window, e.g. a burst of legitimate traffic, a flood of unique assertions, or an attack flooding the endpoint with valid-format assertions, after _cleanup_expired_jtis() reclaims nothing (identity_assertion.py:462-467).

Common situations: High-volume production traffic with the default cache size too small; assertion TTLs much longer than the validation rate makes room for; a denial-of-service flooding valid assertions; load tests exceeding capacity.

Related errors


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/d0a43da4bd5baa66. Report an issue: GitHub.