Перейти к содержимому
Все статьи
· Обновлено 18 апреля 2026 г.

Лимиты Discord Webhook (429, Retry-After и правильный backoff)

Полный гайд по rate limits Discord webhook. Как читать 429, парсить Retry-After, реализовать экспоненциальный backoff и избежать глобального бана.

лимитыrate limits429retry-afterwebhookdiscord apiexponential backoff
Лимиты 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-ResetUnix 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 Webhook