# Rate limits

What the Filudi API limits, by IP, player, email or key, and how a client reads the 429 answer.

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

| 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

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.

```js
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

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.
