Лимиты Discord Webhook (429, Retry-After и правильный backoff)
Полный гайд по rate limits Discord webhook. Как читать 429, парсить Retry-After, реализовать экспоненциальный backoff и избежать глобального бана.
Если ваш Discord webhook начал отвечать 429 Too Many Requests — это не баг, а Discord просит снизить темп. Если игнорировать — получите глобальный бан (10 минут тишины на всех webhook’ах с вашего IP). Если делать правильно — можно стабильно слать тысячи сообщений в минуту.
В этом гайде — точные лимиты Discord webhook на 2026 год, какие заголовки нужно читать и как написать retry-логику, которая выдерживает всплески и долгую нагрузку.
TL;DR — цифры, которые надо знать
- На один webhook: ~30 запросов в 60 секунд (на URL webhook’а)
- На канал: 5 запросов в 5 секунд (общий для всех webhook’ов в канале)
- Глобальный: 50 запросов в секунду (на IP / токен)
- При 429: читай
Retry-After, ждёшь столько секунд, потом ретраишь - При
X-RateLimit-Global: true: останови все запросы — не только тот, что упал - Бан Cloudflare: больше ~10 000 невалидных запросов за 10 минут → IP блокируется на час
Всегда парси заголовки. Хардкод sleep — плохо.
Как работают rate limit headers
Каждый ответ webhook содержит:
X-RateLimit-Limit: 5
X-RateLimit-Remaining: 4
X-RateLimit-Reset: 1714499200.123
X-RateLimit-Reset-After: 1.234
X-RateLimit-Bucket: 80c17d2f203122d936070c88c8d10f33
| Заголовок | Что означает |
|---|---|
X-RateLimit-Limit | Сколько запросов всего разрешено в этом bucket |
X-RateLimit-Remaining | Сколько осталось до лимита |
X-RateLimit-Reset | Unix timestamp (сек), когда bucket обновится |
X-RateLimit-Reset-After | Сколько секунд до обновления (предпочтительно) |
X-RateLimit-Bucket | Хэш bucket’а — группируй запросы по нему, не по URL |
Используй X-RateLimit-Reset-After, не X-RateLimit-Reset. Первый считается на стороне Discord и не зависит от рассинхрона часов на твоей машине.
Тело ответа 429
При превышении лимита приходит HTTP 429 с JSON:
{
"message": "You are being rate limited.",
"retry_after": 0.523,
"global": false,
"code": 0
}
retry_after— в секундах (float, миллисекундная точность с 2020)global: true— пробит глобальный лимит 50/с, тормози всёcode: 30007— Cloudflare бан, ты слишком часто шлёшь невалидные запросы
Минимальный безопасный sender (Python)
import time
import requests
WEBHOOK_URL = "https://discord.com/api/webhooks/ID/TOKEN"
def send(content: str, max_retries: int = 5):
payload = {"content": content}
for attempt in range(max_retries):
r = requests.post(WEBHOOK_URL, json=payload, timeout=10)
# Успех
if r.status_code in (200, 204):
remaining = r.headers.get("X-RateLimit-Remaining")
reset_after = r.headers.get("X-RateLimit-Reset-After")
# Заранее притормозим, если bucket пустой
if remaining == "0" and reset_after:
time.sleep(float(reset_after) + 0.05)
return True
# Rate limit
if r.status_code == 429:
data = r.json()
wait = float(data.get("retry_after", 1))
is_global = data.get("global", False)
print(f"429 — жду {wait:.2f}с (global={is_global})")
time.sleep(wait + 0.05)
continue
# Серверная ошибка → exponential backoff
if 500 <= r.status_code < 600:
backoff = (2 ** attempt) + 0.5
time.sleep(backoff)
continue
# Bad request — ретраить бессмысленно
r.raise_for_status()
return False
send("Production deploy completed")
Ключевые особенности:
- Уважает
retry_afterиз тела 429 - Добавляет 50 мс буфер от джиттера
- Заранее тормозит, когда
X-RateLimit-Remaining= 0 - Экспоненциальный backoff на 5xx
Per-channel vs per-webhook buckets
Тут многие путаются. Два разных webhook’а в один канал делят per-channel лимит. То есть:
- Webhook A →
#alerts(CI шлёт) - Webhook B →
#alerts(мониторинг шлёт)
…и оба бурстят одновременно — поймаете 5 запросов / 5 секунд быстрее, чем ожидали.
Решение: разводить разные классы сообщений по разным каналам, или сериализовать отправку через один воркер на канал.
Exponential backoff с jitter (JavaScript)
Для систем с высоким throughput добавь jitter, чтобы воркеры не ретраили в унисон:
async function send(payload, attempt = 0) {
const res = await fetch(process.env.WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (res.ok) return res;
if (res.status === 429) {
const body = await res.json();
const wait = (body.retry_after + Math.random() * 0.1) * 1000;
await new Promise(r => setTimeout(r, wait));
return send(payload, attempt + 1);
}
if (res.status >= 500 && attempt < 5) {
const backoff = (2 ** attempt + Math.random()) * 1000;
await new Promise(r => setTimeout(r, backoff));
return send(payload, attempt + 1);
}
throw new Error(`Webhook failed: ${res.status}`);
}
Math.random() в задержке предотвращает thundering herd, когда много клиентов ретраят одновременно.
Token bucket — production-паттерн
Для устойчивой отправки реализуй локальный token bucket:
import time
import threading
from collections import deque
class WebhookLimiter:
def __init__(self, max_per_window: int = 30, window_s: float = 60.0):
self.max = max_per_window
self.window = window_s
self.timestamps: deque[float] = deque()
self.lock = threading.Lock()
def acquire(self):
with self.lock:
now = time.monotonic()
# Сбрасываем устаревшие
while self.timestamps and now - self.timestamps[0] > self.window:
self.timestamps.popleft()
if len(self.timestamps) >= self.max:
wait = self.window - (now - self.timestamps[0]) + 0.01
time.sleep(wait)
return self.acquire()
self.timestamps.append(now)
limiter = WebhookLimiter(max_per_window=25) # запас
def send(content: str):
limiter.acquire()
requests.post(WEBHOOK_URL, json={"content": content})
Гарантирует, что вы никогда не отправите больше 25/мин, независимо от latency, ретраев и числа потоков.
Чек-лист: как избежать бана Cloudflare
- Всегда парсить
Retry-After— никогда хардкод -
global: true= full-stop для всех webhook’ов, не только упавшего - Валидировать payload до отправки (не давать Discord возможности отклонить)
- Один queue на канал, не на webhook
- Логировать частоту 429 — если > 1% запросов, логика отправки сломана
- Кэшировать bucket из
X-RateLimit-Bucketесли нужно делить state между процессами
Частые ошибки
Ошибка 1: Считать retry_after миллисекундами. Там секунды (с 2020). X-RateLimit-Reset-After — тоже секунды. Умножение на 1000 = ждёшь в 1000 раз дольше, выглядит как будто ретрай не работает.
Ошибка 2: Ретраить 4xx ошибки. Только 429, 500, 502, 503, 504 ретраятся. 400 Bad Request = payload невалидный, чини его. Долбёжка ускоряет бан Cloudflare.
Ошибка 3: Поток на сообщение. Все потоки делят один IP и один rate limit pool. Параллелизма не получаешь — получаешь больше 429.
Ошибка 4: Игнорировать X-RateLimit-Remaining: 0. Следующий запрос упадёт. Тормози заранее.
Проверь в нашем конструкторе
Хотите увидеть, какой payload реально отправляется? Откройте конструктор Discord Webhook, соберите embed, нажмите «Отправить», посмотрите Network — там видно, какие заголовки возвращает Discord и можно отлаживать retry-логику на живых ответах.
Для автоматизации с высокой нагрузкой посмотрите гайды scheduled messages и automation workflows.
Источники
- Discord developer docs: Rate Limits
- Discord developer docs: Execute Webhook
Попробуйте в нашем инструменте
Открыть конструктор Discord Webhook