toeverything/AFFiNE · warning · TooManyRequest

too_many_request

too_many_request

Error message

Too many requests.

What it means

Thrown by SessionExchangeService.refresh when more than 30 refresh attempts are made within a 60-second sliding window keyed by the refresh token's selector (the segment after the first '.'). Category 'too_many_requests', code 'too_many_request'. The counter is incremented via cache.increaseWithTtl and is per-selector, so it targets a single (possibly compromised) token, not a whole IP.

Source

Thrown at packages/backend/server/src/core/auth/session-exchange.ts:116

      userSessionId: userSession.id,
      ...metadata,
    });
    return this.tokenPair(
      payload.userId,
      issued.session.id,
      issued.refreshToken,
      issued.refreshExpiresAt,
      issued.session.absoluteExpiresAt
    );
  }

  async refresh(req: Request, refreshToken: string, appVersion?: string) {
    if (!isNativeClientRequest(req)) throw new ActionForbidden();
    const selector = refreshToken.split('.')[1];
    if (selector) {
      const rateKey = `auth:session-refresh-rate:${selector}`;
      const attempts = await this.cache.increaseWithTtl(rateKey, 60_000);
      if (attempts > 30) throw new TooManyRequest();
    }
    const refreshed = await this.authSessions.refresh(refreshToken, appVersion);
    if (refreshed.status !== 'rotated') {
      const status =
        refreshed.code === AuthSessionErrorCode.temporarilyUnavailable
          ? HttpStatus.SERVICE_UNAVAILABLE
          : HttpStatus.UNAUTHORIZED;
      throw new AuthSessionHttpError(refreshed.code, status);
    }
    const session = await this.authSessions.get(refreshed.authSessionId);
    if (!session) {
      throw new AuthSessionHttpError(AuthSessionErrorCode.revoked);
    }
    return this.tokenPair(
      session.userSession.userId,
      refreshed.authSessionId,
      refreshed.refreshToken,
      refreshed.refreshExpiresAt,

View on GitHub (pinned to 26c515e050)

Solutions

  1. Stop the refresh retry loop: only refresh once per access-token expiry, and treat a 429 by backing off (not by retrying immediately).
  2. Ensure each device/tab holds its own session and refresh token rather than sharing one.
  3. If the token leaked, revoke the session and force a fresh sign-in.

Example fix

// before: retry on any non-200, including 429
while (!(await refresh()).ok) { /* loops forever */ }

// after: respect 429 with backoff and no immediate retry
const res = await refresh();
if (res.status === 429) {
  const retryAfter = Number(res.headers.get('retry-after') ?? 60);
  await sleep(retryAfter * 1000);
}
Defensive patterns

Strategy: retry

Validate before calling

const REFRESH_WINDOW_MS = 60_000;
const REFRESH_MAX = 30;
function canRefreshNow(lastAttempts: number[]): boolean {
  const now = Date.now();
  const recent = lastAttempts.filter(t => now - t < REFRESH_WINDOW_MS);
  return recent.length < REFRESH_MAX;
}

Type guard

function hasSelector(refreshToken: string): boolean {
  const seg = refreshToken.split('.')[1];
  return typeof seg === 'string' && seg.length > 0;
}

Try / catch

try {
  await refresh(req, refreshToken, appVersion);
} catch (e) {
  if (e.code === 'too_many_request') {
    const retryAfter = 60; // selector window is 60s
    await sleep(retryAfter * 1000);
    // then retry at most once; if it 429s again, sign the user in fresh
  } else throw e;
}

Prevention

When it happens

Trigger: Calling refresh (session-exchange.ts:112-116) more than 30 times in 60s with a refresh token whose selector portion is identical. Each call increments the counter regardless of success.

Common situations: A bug in the client causing a refresh loop (e.g. retrying on every 401, including the 401 from this very rate limit); a malicious actor hammering a leaked refresh token; multiple app instances/tabs all refreshing at once with the same token; an aggressive background poller.

Understand the failure class

Related errors


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/969efccea3bf43f3. Report an issue: GitHub.