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
- Stop the refresh retry loop: only refresh once per access-token expiry, and treat a 429 by backing off (not by retrying immediately).
- Ensure each device/tab holds its own session and refresh token rather than sharing one.
- 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
- Refresh at most once per access-token expiry, not on a fixed short interval.
- Treat a 429 as a signal to back off, never to retry immediately in a loop.
- Give each device/tab its own session rather than sharing one refresh token.
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
- HTTP status errors: handling 4xx and 5xx responses — how to handle 4xx and 5xx responses properly.
Related errors
- The refresh token is invalid. | The auth session has expired
- auth_session_revoked
- action_forbidden
- action_forbidden
- invalid_auth_state
AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12).
Data as JSON: /api/errors/969efccea3bf43f3.
Report an issue: GitHub.