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.
Per route
Section titled “Per route”| Route | Limit |
|---|---|
POST /v1/sdk/players | 20 per hour per IP. Create guests in the browser, where each visitor has their own IP |
POST /v1/sdk/game-tokens | 240 per player per hour |
POST /v1/sdk/leaderboards/{gameId}/{board}/scores | ratePerMinute of the board, 10 by default; a nonce is accepted once per session |
POST /v1/sdk/events | 50 items per batch, 64 kB per body |
PUT /v1/sdk/games/{gameId}/rating | 60 ratings per player per hour |
POST /v1/sdk/games/{gameId}/comments | 5 comments per player per hour |
PATCH /v1/sdk/comments/{commentId} | 30 edits per player per hour |
POST /v1/account/magic-link | 3 links per email per hour |
POST /v1/account/magic-link/consume-code | 20 attempts per IP per hour; the fifth wrong code burns the link |
POST /v1/account/sign-in, POST /v1/console/sign-in | Attempts counted per email and per IP; unknown email and wrong password answer the same 401 in the same time |
POST /v1/feedback | 5 per IP per hour and 10 per player per day |
POST /v1/console/invitation-requests | 5 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.
What a client does
Section titled “What a client does”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.
Caches you can lean on
Section titled “Caches you can lean on”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.