Rate limits
Limits keep one misbehaving client from slowing everybody down. They work the way Discord's do, so a library that reads the headers works unchanged.
How they work
Every call belongs to a bucket: a kind of call on one channel or one server. Each bucket allows a number of calls in a window. Separate channels and separate servers have separate buckets, so a busy channel does not slow another. On top of the buckets, one application may make 50 calls a second in all.
Limits are counted per application, not per address: an application is the unit that is responsible, and a hosting provider's address is shared by strangers.
Headers
Every answer carries:
| Header | |
|---|---|
X-RateLimit-Limit | Calls allowed in the window of this bucket |
X-RateLimit-Remaining | Calls left in this window |
X-RateLimit-Reset | When the window ends, in seconds since 1970 (with decimals) |
X-RateLimit-Reset-After | Seconds until it ends |
X-RateLimit-Bucket | A name for the bucket: calls with the same name share a limit |
Over the limit the answer is 429 with Retry-After (whole seconds), X-RateLimit-Scope (user: your bucket; global: the 50-a-second limit) and a body:
Response429Too Many RequestsHTTP
http
HTTP/1.1 429 Too Many Requests
Retry-After: 2
X-RateLimit-Scope: user
X-RateLimit-Limit: 5
X-RateLimit-Remaining: 0
X-RateLimit-Reset-After: 1.724
Content-Type: application/json
{
"message": "You are being rate limited.",
"retry_after": 1.724,
"global": false
}retry_after is in seconds and has decimals; Retry-After is the same time rounded up.
Wait, then try again
js
async function call(url, options, attempt = 0) {
const response = await fetch(url, options)
if (response.status !== 429 || attempt >= 5) return response
const { retry_after } = await response.json()
await new Promise(done => setTimeout(done, retry_after * 1000))
return call(url, options, attempt + 1)
}python
import time
import requests
def call(method, url, attempts=5, **options):
for _ in range(attempts):
response = requests.request(method, url, **options)
if response.status_code != 429:
break
time.sleep(response.json()["retry_after"])
return responseThe buckets
| Calls | Limit | Per |
|---|---|---|
Create a message (POST /channels/{id}/messages) | 5 in 5 seconds | channel |
| Edit or delete a message | 5 in 5 seconds | channel |
| Read messages | 30 in 10 seconds | channel |
| Reactions | 20 in 5 seconds | channel |
| Typing | 5 in 10 seconds | channel |
| Pins | 10 in 10 seconds | channel |
| Bans and changes to members | 10 in 10 seconds | server |
| Creating, changing or deleting channels, roles, commands | 10 in 10 seconds | server or channel |
| Everything else | 30 in 10 seconds | route |
| All calls together | 50 in 1 second | application |
The limits are in the response headers: do not hard-code the numbers, read the headers.
Other limits
| Where | Limit |
|---|---|
POST /api/v1/oauth2/token and /revoke | 60 per minute per IP address |
| A member using commands and buttons | 20 interactions in 10 seconds; autocomplete 30 in 10 seconds |
| Followups to one interaction | 10 in 10 seconds |
| Commands an application may register | 100 slash commands and 15 of each context-menu kind, per scope (details) |
| A bot's messages in all channels | 300 a minute, however the buckets are spread |
Being a good citizen
- Back off exponentially on
429and5xx. HonourRetry-After. - Do not poll for what the gateway (planned) will push.
- Keep one token per bot process, and cache what you read.
- Cache your OAuth2 tokens: an access token lasts an hour, so refresh once, not before every call.