Discord Bot Rate Limits: How 429 Errors Work and How to Avoid a Cloudflare Ban
Guide · · 3 min read · discord, discord-bots, rate-limits, api, bot-hosting
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:
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.