> ## Documentation Index
> Fetch the complete documentation index at: https://docs.discordbotlist.lol/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors and rate limits

> Five status codes, what each one means here, and how to back off.

Every failure is JSON with an `error` field holding one sentence. Nothing in it
is a stack trace, and nothing in it is safe to parse for meaning — read the
status code, log the sentence.

```json theme={null}
{ "error": "Rate limit exceeded.", "retryAfter": 42 }
```

## Status codes

<AccordionGroup>
  <Accordion title="400 — the body or a query parameter is malformed">
    The JSON did not parse, a required field is missing, or a value is outside
    its bounds. `error` names the first problem found rather than listing them
    all.

    Common causes: posting `{"server_count": "1482"}` as a string, a command
    `name` over 32 characters, or `/check` without a `userId`.
  </Accordion>

  <Accordion title="401 — missing, malformed or unknown token">
    No `Authorization` header, a header we cannot read, or a token that is not
    in our database. Rotating a token invalidates the old one immediately, so a
    sudden 401 on a bot that worked yesterday usually means somebody generated a
    new one.
  </Accordion>

  <Accordion title="404 — no such bot, or the token belongs to a different one">
    Also what you get for a bot id that is not a Discord snowflake. It is
    deliberately the same answer in all three cases; see
    [Authentication](/authentication).
  </Accordion>

  <Accordion title="429 — rate limited">
    The body carries `retryAfter`, in **seconds**. Wait that long; retrying
    immediately only spends the same budget again, because the limit is keyed on
    your token rather than on your IP.
  </Accordion>

  <Accordion title="500 — we failed to store it">
    The request was fine and we could not write it. Retry with backoff.
    `GET https://discordbotlist.lol/api/health` answers `ok` or not without a
    credential, which is the quickest way to tell a fault on our side from one
    in your integration.
  </Accordion>
</AccordionGroup>

## Backing off

A working pattern, and the one we would write ourselves:

```js theme={null}
async function post(url, init, attempt = 0) {
  const response = await fetch(url, init);
  if (response.status !== 429 && response.status < 500) return response;
  if (attempt >= 4) return response;

  const body = await response.json().catch(() => ({}));
  // retryAfter is authoritative when present; otherwise exponential with jitter.
  const waitSeconds = body.retryAfter ?? 2 ** attempt + Math.random();
  await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000));
  return post(url, init, attempt + 1);
}
```

<Note>
  Do not retry a `400` or a `401`. Neither will succeed on a second attempt, and
  a bot retrying a malformed body on a loop is how a token ends up rate limited
  for a reason its owner cannot see.
</Note>

## Limits, and why each is what it is

| Endpoint        | Limit per minute | Reasoning                                               |
| --------------- | ---------------- | ------------------------------------------------------- |
| `POST /stats`   | 60               | One post per shard per interval, with headroom.         |
| `PUT /commands` | 10               | Published on startup; only has to clear a restart loop. |
| `GET /check`    | 600              | Called once per command invocation in a busy bot.       |
| `GET /votes`    | 60               | A log you poll.                                         |

If our rate-limit store is unreachable the limiter fails **open**: your requests
go through. A cache outage should not stop a running bot from checking in, and
the token is still verified either way.
