Skip to content

Rate limits

Limits live in Redis and count per IP, per player, per email or per key, depending on the route. Over a limit the API answers 429 rate_limited with a Retry-After header in seconds. When Redis is down, a limited write route answers 503 unavailable instead of guessing.

RouteLimit
POST /v1/sdk/players20 per hour per IP. Create guests in the browser, where each visitor has their own IP
POST /v1/sdk/game-tokens240 per player per hour
POST /v1/sdk/leaderboards/{gameId}/{board}/scoresratePerMinute of the board, 10 by default; a nonce is accepted once per session
POST /v1/sdk/events50 items per batch, 64 kB per body
PUT /v1/sdk/games/{gameId}/rating60 ratings per player per hour
POST /v1/sdk/games/{gameId}/comments5 comments per player per hour
PATCH /v1/sdk/comments/{commentId}30 edits per player per hour
POST /v1/account/magic-link3 links per email per hour
POST /v1/account/magic-link/consume-code20 attempts per IP per hour; the fifth wrong code burns the link
POST /v1/account/sign-in, POST /v1/console/sign-inAttempts counted per email and per IP; unknown email and wrong password answer the same 401 in the same time
POST /v1/feedback5 per IP per hour and 10 per player per day
POST /v1/console/invitation-requests5 per IP per day
/v1/server/*600 requests a minute per key; no IP limit

The admin API has no request limit beyond the quotas of the data itself: 10 pending games and 3 pending sites per account, 20 keys per site, 10 proposed domains, 50 grants per invitation.

Read Retry-After and wait that long before the same request. Do not retry a 429 in a loop: the counter does not reset on refused requests.

async function withRetry(request) {
const response = await request();
if (response.status !== 429) return response;
const seconds = Number(response.headers.get('retry-after') ?? '1');
await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
return request();
}

@filudi/hub-core and @filudi/game-sdk follow this rule on their own. A score refused for rate reasons comes back to the SDK as rejected, never as an exception.

Public reads carry Cache-Control and an ETag. GET /v1/catalog/{site} is public, max-age=300, stale-while-revalidate=600; the reviews of a game are public, max-age=60; a leaderboard top is public, max-age=30. Send If-None-Match and a 304 costs nothing. The statistics routes are cached 60 s in Redis per set of filters, so two identical calls in a minute return the same numbers.