<!-- canonical: https://hostdc.xyz/blog/discord-bot-rate-limits-how-429-errors-work-and-how-to-avoid-a-cloudflare-ban · published 2026-10-11 · HostDC -->

# Discord Bot Rate Limits: How 429 Errors Work and How to Avoid a Cloudflare Ban

_Written with AI help._

## The short answer

Read the rate limit headers on every response, wait when you get a 429, and never hard-code a limit. Discord's API enforces a global cap and separate per-route caps, and a bot that ignores them can end up with its IP temporarily restricted.

## The global limit

A bot can send up to 50 requests per second to the Discord API. Requests without an authorization header are limited per IP address instead. This cap sits on top of the per-route limits, so you can hit either one first. Interaction endpoints are not bound by the global limit.

## Per-route limits and buckets

Most endpoints have their own limit, and it can depend on the HTTP method. Some routes share a limit, and Discord marks them with the same `X-RateLimit-Bucket` value. Top-level resources such as channels, guilds and webhooks are counted separately. Hitting the limit on `/channels/1234` does not affect `/channels/9876`.

Emoji routes are the odd one. They are limited per guild and do not follow the usual conventions, so the quota you read from the headers may be off and you can still get a 429.

## Headers to read

Every response carries the state of its bucket:

- `X-RateLimit-Limit`: how many requests the bucket allows.
- `X-RateLimit-Remaining`: how many are left in the current window.
- `X-RateLimit-Reset`: the epoch time, in seconds, when the limit resets.
- `X-RateLimit-Reset-After`: seconds until the bucket resets. It can be a decimal.
- `X-RateLimit-Bucket`: the ID of the limit.

Two more headers appear only on a 429. `X-RateLimit-Global` shows up when the global limit caused it. `X-RateLimit-Scope` is `user`, `global` or `shared`.

## What a 429 looks like

A rate limited request returns status 429 with a JSON body. The `message` field says you are being rate limited. `retry_after` is a float with the seconds to wait. `global` is a boolean that tells you whether the global limit was hit. Some limits also add an integer `code`. The `Retry-After` header carries the same wait time as `retry_after`.

A minimal handler reads the wait time and sleeps before retrying:

```python
import time
import requests

def call(url, headers):
    while True:
        r = requests.get(url, headers=headers)
        if r.status_code != 429:
            return r
        time.sleep(r.json()["retry_after"])
```

Discord's own docs say not to hard-code limits and to parse the headers instead.

## The invalid request limit

This limit is the one that leads to temporary bans. If an IP sends 10,000 invalid requests in 10 minutes, it is temporarily restricted. An invalid request is one that returns 401, 403 or 429. A 429 with `X-RateLimit-Scope: shared` does not count. Other error statuses can also count: reusing a webhook that returned 404 can lead to a temporary restriction.

The numbers are closer than they look. A sustained rate of about 16 to 17 failing requests per second is enough to reach 10,000 in ten minutes.

## How to stay under the limits

Discord recommends a few habits, and each one targets a specific status code:

- Use a valid token, and stop sending requests once Discord rejects it, to avoid 401 errors.
- Check role and channel permissions before you act, to avoid 403 errors.
- Do not send requests on a bucket that has no requests left until it resets, to avoid 429 errors.
- Stop using webhooks that return 404.
- Log your invalid request rate if your bot is large.

According to Discord, an occasional Cloudflare ban usually comes from an unhandled spike of errors, not from the global limit. If you are banned repeatedly during normal operation, you may be running into the global limit, and you can ask Discord for a higher limit at https://dis.gd/rate-limit.
