Отказоустойчивая система доставки для Telegram
Как построить надёжную систему доставки сообщений через Telegram: retry-логика, circuit breaker, DLQ и мониторинг.
Зачем нужна отказоустойчивость
Telegram Bot API — внешний сервис, который вы не контролируете. Он может временно упасть, вернуть ошибку или ограничить вашего бота. В production-среде единственный вопрос — не «если» что-то сломается, а «когда».
Отказоустойчивая система доставки должна:
- Продолжать работать при временных сбоях Telegram API
- Не терять сообщения при перезапуске процесса
- Автоматически повторять неудачные отправки
- Не «заваливать» API повторными запросами при сбое
- Уведомлять о проблемах и деградации
- Переключаться на альтернативные каналы при длительном сбое
В этой статье мы детально разберём каждый из этих аспектов. Если вы ещё не знакомы с лимитами Telegram, начните со статьи Telegram API лимиты.
Стратегии повторных попыток
Правильный retry — основа отказоустойчивости. Но не все стратегии одинаково полезны:
| Стратегия | Задержка | Плюсы | Минусы |
|---|---|---|---|
| Немедленный retry | 0 мс | Быстрое восстановление при единичной ошибке | Усиливает нагрузку при массовом сбое |
| Фиксированная задержка | N секунд | Простота реализации | Не адаптируется к ситуации |
| Линейный backoff | N × attempt | Плавное увеличение | Слишком медленно снижает нагрузку |
| Экспоненциальный backoff | N × 2^attempt | Подходящий баланс | Может быть слишком агрессивным |
| Backoff + jitter | N × 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:
- Логируйте причину: сохраняйте HTTP-статус, тело ответа и количество попыток
- Классифицируйте ошибки: «бот заблокирован» ≠ «таймаут». Первое — перманентно, второе — временное
- Настройте алерты: если DLQ растёт быстрее обычного — что-то не так с системой
- Периодически обрабатывайте: запускайте job для анализа DLQ раз в час/день
- Удаляйте перманентные ошибки: заблокированных пользователей — помечайте в базе данных
Идемпотентность доставки
При 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-call | ☐ | Runbook для типичных инцидентов |
Или просто используйте Релая. Все пункты выше — уже реализованы в платформе: retry, circuit breaker, DLQ, мониторинг, резервирование через MAX и другие каналы. Вместо недель разработки инфраструктуры — подключение за 30 минут.
Отказоустойчивость — не опция, а требование для любой production-системы. Используйте паттерны из этой статьи или доверьте инфраструктуру Релая. Также можно прочитать о массовой отправке в Telegram и мультиканальной доставке.
Создайте бесплатный MAX-профиль
Если хочется не просто читать, а сразу проверить сценарий руками: подключите MAX, отправьте себе тестовое сообщение и уже потом решайте, нужны ли другие каналы.