Telegram
11 мин·

Отказоустойчивая система доставки для Telegram

Как построить надёжную систему доставки сообщений через Telegram: retry-логика, circuit breaker, DLQ и мониторинг.

Зачем нужна отказоустойчивость

Telegram Bot API — внешний сервис, который вы не контролируете. Он может временно упасть, вернуть ошибку или ограничить вашего бота. В production-среде единственный вопрос — не «если» что-то сломается, а «когда».

Отказоустойчивая система доставки должна:

  • Продолжать работать при временных сбоях Telegram API
  • Не терять сообщения при перезапуске процесса
  • Автоматически повторять неудачные отправки
  • Не «заваливать» API повторными запросами при сбое
  • Уведомлять о проблемах и деградации
  • Переключаться на альтернативные каналы при длительном сбое

В этой статье мы детально разберём каждый из этих аспектов. Если вы ещё не знакомы с лимитами Telegram, начните со статьи Telegram API лимиты.

Стратегии повторных попыток

Правильный retry — основа отказоустойчивости. Но не все стратегии одинаково полезны:

СтратегияЗадержкаПлюсыМинусы
Немедленный retry0 мсБыстрое восстановление при единичной ошибкеУсиливает нагрузку при массовом сбое
Фиксированная задержкаN секундПростота реализацииНе адаптируется к ситуации
Линейный backoffN × attemptПлавное увеличениеСлишком медленно снижает нагрузку
Экспоненциальный backoffN × 2^attemptПодходящий балансМожет быть слишком агрессивным
Backoff + jitterN × 2^attempt ± randomЛучший вариант для productionЧуть сложнее реализации
Золотое правило: всегда используйте экспоненциальный backoff с jitter. Это предотвращает «thundering herd» — ситуацию, когда все клиенты одновременно пытаются повторить запрос.

Когда НЕ стоит повторять

Не все ошибки стоит ретраить. Вот карта решений:

  • 429 (Too Many Requests): ретраить через retry_after — обязательно
  • 500, 502, 503: ретраить с backoff — серверная ошибка Telegram
  • 400 (Bad Request): НЕ ретраить — ваш запрос невалиден
  • 401 (Unauthorized): НЕ ретраить — невалидный токен
  • 403 (Forbidden): НЕ ретраить — бот заблокирован пользователем
  • Сетевая ошибка (timeout): ретраить с backoff

Паттерн Circuit Breaker

Circuit Breaker — защитный паттерн, предотвращающий каскадные отказы. Идея взята из электротехники: когда ток превышает допустимый — автомат размыкает цепь.

Рендер схемы...

Реализация на TypeScript:

// Circuit Breaker для Telegram API
interface CircuitBreakerOptions {
  failureThreshold: number;  // ошибок до размыкания
  resetTimeout: number;      // мс до перехода в HALF-OPEN
  monitorWindow: number;     // мс окна мониторинга
}

type CircuitState = "CLOSED" | "OPEN" | "HALF_OPEN";

class CircuitBreaker {
  private state: CircuitState = "CLOSED";
  private failures: number[] = [];
  private lastFailure: number = 0;
  private options: CircuitBreakerOptions;

  constructor(options: CircuitBreakerOptions) {
    this.options = options;
  }

  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === "OPEN") {
      // Проверяем, прошло ли достаточно времени
      if (Date.now() - this.lastFailure > this.options.resetTimeout) {
        this.state = "HALF_OPEN";
        console.log("[CB] Переход в HALF_OPEN, пробуем...");
      } else {
        throw new Error("Circuit breaker is OPEN — запросы заблокированы");
      }
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (err) {
      this.onFailure();
      throw err;
    }
  }

  private onSuccess(): void {
    if (this.state === "HALF_OPEN") {
      console.log("[CB] Тестовый запрос успешен → CLOSED");
      this.state = "CLOSED";
      this.failures = [];
    }
  }

  private onFailure(): void {
    this.lastFailure = Date.now();

    // Удаляем старые ошибки за пределами окна
    const windowStart = Date.now() - this.options.monitorWindow;
    this.failures = this.failures.filter((t) => t > windowStart);
    this.failures.push(Date.now());

    if (this.state === "HALF_OPEN") {
      console.log("[CB] Тестовый запрос неуспешен → OPEN");
      this.state = "OPEN";
      return;
    }

    if (this.failures.length >= this.options.failureThreshold) {
      console.log(
        `[CB] ${this.failures.length} ошибок за ` +
        `${this.options.monitorWindow}ms → OPEN`
      );
      this.state = "OPEN";
    }
  }

  getState(): CircuitState {
    return this.state;
  }
}

// Использование с Telegram API
const breaker = new CircuitBreaker({
  failureThreshold: 5,   // 5 ошибок
  resetTimeout: 30000,    // ждём 30 сек перед пробой
  monitorWindow: 60000,   // в окне 60 сек
});

async function sendViaTelegram(chatId: number, text: string) {
  return breaker.execute(async () => {
    const res = await fetch(
      `https://api.telegram.org/bot${TOKEN}/sendMessage`,
      {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ chat_id: chatId, text }),
      }
    );

    if (!res.ok && res.status !== 429) {
      throw new Error(`Telegram error: ${res.status}`);
    }

    if (res.status === 429) {
      const body = await res.json();
      // 429 — это ожидаемый rate limit, не считаем как failure
      await sleep(body.parameters.retry_after * 1000);
      throw new Error("Rate limited (retryable)");
    }

    return res.json();
  });
}

Dead Letter Queue

Dead Letter Queue (DLQ) — хранилище для сообщений, которые не удалось доставить после всех повторных попыток. Это критический компонент отказоустойчивой системы. Подробнее о DLQ в контексте массовых рассылок — в статье массовая отправка в Telegram.

Ключевые правила работы с DLQ:

  1. Логируйте причину: сохраняйте HTTP-статус, тело ответа и количество попыток
  2. Классифицируйте ошибки: «бот заблокирован» ≠ «таймаут». Первое — перманентно, второе — временное
  3. Настройте алерты: если DLQ растёт быстрее обычного — что-то не так с системой
  4. Периодически обрабатывайте: запускайте job для анализа DLQ раз в час/день
  5. Удаляйте перманентные ошибки: заблокированных пользователей — помечайте в базе данных

Идемпотентность доставки

При retry есть риск отправить сообщение дважды. Например: запрос отправлен, Telegram принял его, но ответ потерялся — и ваша система делает retry. Результат: пользователь получает два одинаковых сообщения.

Стратегии обеспечения идемпотентности:

  • Уникальный идентификатор сообщения: храните флаг «отправлено» в базе данных до попытки отправки. Перед retry проверяйте флаг.
  • Дедупликация по message_id: если Telegram вернул message_id — сохраните его. При retry проверьте, нет ли уже такого message_id.
  • Идемпотентный ключ на уровне очереди: BullMQ поддерживаетjobId — задачи с одинаковым ID не дублируются.
// Идемпотентная отправка с проверкой в Redis
async function sendIdempotent(chatId, text, messageKey) {
  const redis = getRedisClient();
  const lockKey = `sent:${messageKey}`;

  // Проверяем, было ли уже отправлено
  const alreadySent = await redis.get(lockKey);
  if (alreadySent) {
    console.log(`Сообщение ${messageKey} уже отправлено (message_id: ${alreadySent})`);
    return JSON.parse(alreadySent);
  }

  // Ставим временную блокировку (TTL 5 минут)
  const locked = await redis.set(lockKey, "pending", "NX", "EX", 300);
  if (!locked) {
    console.log(`Сообщение ${messageKey} уже в процессе отправки`);
    return null;
  }

  try {
    const result = await sendTelegramMessage(chatId, text);
    // Сохраняем результат на 24 часа
    await redis.set(lockKey, JSON.stringify(result), "EX", 86400);
    return result;
  } catch (err) {
    // Удаляем блокировку при ошибке
    await redis.del(lockKey);
    throw err;
  }
}

Health checks и мониторинг

Мониторинг — глаза и уши вашей системы. Без него вы узнаёте о проблемах последними — от пользователей. Ключевые проверки:

ПроверкаЧастотаДействие при сбое
Telegram API доступенКаждые 30 секПереключить на резервный канал
Webhook доставляет обновленияКаждые 60 секПереключиться на polling
Очередь не переполненаКаждые 10 секУвеличить воркеров или алерт
DLQ не растёт аномальноКаждые 5 минАлерт в Slack/Telegram
Circuit breaker не в OPENПостоянноАлерт + резервный канал
Бот не заблокированКаждые 5 минАлерт критического уровня
// Статистика отказоустойчивой системы — dashboard API
const stats = await fetch('/api/stats/resilience');
// {
//   "circuit_breaker": "CLOSED",
//   "dlq_size": 15,
//   "queue_depth": 230,
//   "success_rate_5m": 0.987,
//   "api_healthy": true,
//   "retries": { "level_1": 340, "level_2": 45, "level_3": 8 }
// }

Graceful degradation

Graceful degradation — способность системы продолжать работать с пониженным функционалом при частичном отказе. Примеры:

  • Telegram API медленный: снижаем скорость отправки с 25 msg/sec до 10 msg/sec, увеличиваем timeout
  • Rate limit достигнут: останавливаем маркетинговые рассылки, оставляем только транзакционные уведомления
  • Telegram полностью недоступен: переключаемся на альтернативный канал (MAX, email, SMS)
  • Redis недоступен: переходим на in-memory очередь с ограниченным размером, Логируем в файл
Принцип: лучше доставить 80% сообщений с небольшой задержкой, чем потерять 100% из-за каскадного отказа. Проектируйте систему так, чтобы каждый компонент мог деградировать независимо.

Резервные каналы

Самая мощная стратегия отказоустойчивости — мультиканальность. Если Telegram недоступен, критические сообщения должны дойти через другой канал. Подробнее — в статье устойчивая система доставки.

Рендер схемы...

MAX — отличный выбор как резервный канал для Telegram:

  • Растущая аудитория в России — пользователи всё чаще используют MAX
  • Более предсказуемые лимиты — официальная документация с точными значениями
  • Удобный Bot API с похожей моделью взаимодействия
  • Через Релая переключение между каналами — одна строка конфигурации

Production-чеклист

Перед выходом в production убедитесь, что ваша система доставки отвечает следующим требованиям:

ТребованиеСтатусКомментарий
Retry с exponential backoff + jitterОбязательно для 429 и 5xx
Circuit breaker защищает APIПредотвращает каскадные отказы
Dead Letter Queue настроенаС алертами и периодической обработкой
Идемпотентность обеспеченаЧерез уникальные ключи или дедупликацию
Health checks работаютTelegram API, очередь, DLQ
Мониторинг и алерты настроеныДашборд аналитики
Graceful degradation реализованПриоритет транзакционных сообщений
Резервный канал настроенMAX, VK или email через Релая
Нагрузочное тестирование проведеноMock-сервер + реальный объём
Документация для on-callRunbook для типичных инцидентов
Или просто используйте Релая. Все пункты выше — уже реализованы в платформе: retry, circuit breaker, DLQ, мониторинг, резервирование через MAX и другие каналы. Вместо недель разработки инфраструктуры — подключение за 30 минут.

Отказоустойчивость — не опция, а требование для любой production-системы. Используйте паттерны из этой статьи или доверьте инфраструктуру Релая. Также можно прочитать о массовой отправке в Telegram и мультиканальной доставке.

Создайте бесплатный MAX-профиль

Если хочется не просто читать, а сразу проверить сценарий руками: подключите MAX, отправьте себе тестовое сообщение и уже потом решайте, нужны ли другие каналы.