Лид: Очередь сообщений, повторные попытки, идемпотентность, ограничения API и контроль фактической доставки.

Telegram-бот для уведомлений: надёжная доставка и защита от дублей
Telegram-бот для уведомлений: надёжная доставка и защита от дублей

Устойчивую доставку служебных уведомлений через Telegram строят на четырёх элементах: очередь для буферизации событий, повторные попытки при сбоях, защита от дублей через идемпотентность и мониторинг фактической доставки. Telegram Bot API предоставляет HTTP-интерфейс для отправки сообщений, но не гарантирует доставку при проблемах сети или превышении лимитов, поэтому архитектуру надёжности встраивают в приложение.

Задача и её специфика

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

Telegram Bot API работает по модели «выстрелил и забыл»: метод sendMessage возвращает успех или ошибку, но не контролирует чтение сообщения адресатом. Сетевые таймауты, перегрузка API и блокировки бота со стороны получателя вызывают временные сбои, которые требуют повторных попыток. При этом повторная отправка без контроля создаёт дубли, если первая попытка фактически прошла, но ответ потерялся.

Архитектура надёжной доставки начинается с разделения генерации события и отправки сообщения через очередь, добавляет логику повторов с экспоненциальной задержкой, обеспечивает идемпотентность через уникальные идентификаторы сообщений и завершается мониторингом статуса доставки.

События и очередь сообщений

Компонент, обнаруживающий событие — мониторинг, сервис обработки данных, планировщик задач — не должен отправлять уведомление напрямую. Синхронный вызов sendMessage блокирует поток на время сетевого запроса (от 100 мс до нескольких секунд), а при сбое требует обработки исключений внутри основной логики. Генерация события и доставка уведомления — разные зоны ответственности с разными требованиями к надёжности.

Очередь сообщений разделяет эти операции. Компонент-источник записывает событие в очередь: идентификатор события, тип, временную метку, целевой chat ID и текст сообщения. Запись в очередь — быстрая локальная операция, которая завершается за единицы миллисекунд. Отдельный процесс-воркер читает события из очереди, вызывает Telegram Bot API и обрабатывает результат.

Варианты реализации очереди зависят от масштаба. Для десятков сообщений в час подойдёт таблица PostgreSQL с полем статуса и индексом по временной метке. Воркер опрашивает таблицу каждые несколько секунд, выбирает записи в статусе «ожидает», обновляет статус на «отправляется» и вызывает API. Для сотен сообщений в минуту используют Redis Streams или выделенные брокеры сообщений (RabbitMQ, Apache Kafka), которые гарантируют доставку события воркеру.

Структура записи в очереди

Каждая запись содержит:

  • Уникальный идентификатор события — UUID или составной ключ (тип_события + объект + временная_метка), который используется для идемпотентности.
  • Целевой chat_id — идентификатор получателя в Telegram, полученный через предварительную подписку пользователя на бота.
  • Текст сообщения — уже сформированное тело уведомления, чтобы воркер не зависел от внешних данных при отправке.
  • Приоритет — опционально, для разделения критических уведомлений и второстепенных.
  • Временная метка создания — для отслеживания задержек и очистки устаревших событий.
  • Счётчик попыток — количество предпринятых повторных отправок.
  • Статус — «ожидает», «отправляется», «доставлено», «ошибка», «отброшено».

Повторные попытки при сбоях

Telegram Bot API может вернуть ошибку из-за сетевого таймаута, ограничения частоты, временной недоступности или блокировки бота пользователем. Не зашивайте в код неофициальные «глобальные» числа: при ответе 429 Bot API может вернуть parameters.retry_after, и именно его нужно соблюдать.

Воркер анализирует HTTP-статус и тело ответа. Коды 429 (Too Many Requests) и 5xx указывают на временные проблемы — перегрузку или сбой на стороне Telegram. Код 400 с описанием «chat not found» или «bot was blocked by the user» означает постоянную ошибку: повторная попытка бессмысленна. Код 200 подтверждает успешную отправку.

Для временных ошибок применяют экспоненциальную задержку (exponential backoff): первая повторная попытка через 1 секунду, вторая через 2 секунды, третья через 4, четвёртая через 8, далее ограничивают максимальной задержкой (например, 60 секунд). Добавление случайного джиттера (jitter) — смещения на случайное число миллисекунд — предотвращает одновременные повторы большой группы сообщений после общего сбоя.

Максимальное количество попыток ограничивают (типично 5–10), после чего сообщение переводят в статус «отброшено» и отправляют алерт администраторам. Для критических уведомлений предусматривают запасной канал: email, SMS, запись в служебный чат для ручной проверки.

HTTP-статус Описание ошибки Тип Действие
200 OK Успех Статус «доставлено», завершить обработку
400 Bad Request: chat not found Постоянная Статус «ошибка», уведомить администратора, не повторять
400 Bad Request: bot was blocked by the user Постоянная Статус «ошибка», удалить подписку, не повторять
429 Too Many Requests Временная Прочитать заголовок Retry-After, повторить через указанное время
500, 502, 503 Internal Server Error, Bad Gateway, Service Unavailable Временная Повторить с экспоненциальной задержкой
Timeout Сетевой таймаут Неопределённая Повторить, полагаясь на идемпотентность
Идемпотентность и защита от дублей
Идемпотентность и защита от дублей

Идемпотентность и защита от дублей

Сетевой таймаут создаёт неопределённость: воркер отправил запрос, Telegram принял его и доставил сообщение, но ответ потерялся из-за проблем соединения. Воркер считает попытку неудачной и повторяет отправку, создавая дубль. Идемпотентность — свойство операции давать один и тот же результат при повторных выполнениях с одними и теми же параметрами.

Telegram Bot API не предоставляет встроенной дедупликации на уровне API. Метод sendMessage не принимает параметр вроде idempotency_key. Защиту от дублей встраивают на стороне приложения через кэш отправленных сообщений.

Перед вызовом API воркер проверяет кэш: отправлялось ли сообщение с данным уникальным идентификатором события. Если запись существует и статус «доставлено», повторный вызов пропускают. Если запись существует и статус «отправляется», проверяют время последней попытки: если прошло больше таймаута (например, 30 секунд), считают попытку зависшей и повторяют, иначе ждут.

Кэш реализуют через Redis с временем жизни ключа (TTL) равным максимальному времени обработки события плюс запас (например, 1 час для событий, которые актуальны в течение 10 минут). Ключом служит уникальный идентификатор события, значением — JSON с chat_id, временем отправки и статусом. Атомарные операции Redis (SETNX, SET с опцией NX) предотвращают race condition при параллельной работе нескольких воркеров.

Алгоритм идемпотентной отправки

  1. Воркер извлекает событие из очереди, получает его уникальный идентификатор event_id.
  2. Выполняет GET idempotency:event_id в Redis.
  3. Если ключ существует и содержит статус «доставлено», помечает событие как обработанное, удаляет из очереди.
  4. Если ключ не существует, выполняет SET idempotency:event_id '{"status":"sending","timestamp":...}' NX EX 3600. Если операция вернула успех, продолжает. Если вернула null (ключ уже установлен другим воркером), переходит к шагу 2.
  5. Вызывает sendMessage через Bot API.
  6. Если ответ 200 OK, обновляет ключ: SET idempotency:event_id '{"status":"delivered","timestamp":...}', помечает событие как доставленное, удаляет из очереди.
  7. Если временная ошибка, увеличивает счётчик попыток, возвращает событие в очередь с задержкой.
  8. Если постоянная ошибка, обновляет статус на «ошибка», уведомляет администратора, удаляет из очереди.

Ограничения API и rate limiting

Telegram Bot API ограничивает частоту отправки, но прикладной код должен опираться на фактический ответ 429 и поле parameters.retry_after в JSON-ответе. Значение зависит от контекста и текущей политики Telegram, поэтому жёсткие числа в тексте и коде не нужны.

Воркер отслеживает собственную скорость отправки через скользящее окно или алгоритм token bucket. Token bucket моделирует «ведро токенов», которое пополняется с постоянной скоростью (например, 25 токенов в секунду для запаса под 30 сообщений/с) и тратится на каждую отправку. Если токенов недостаточно, воркер ожидает их пополнения перед следующей отправкой.

При 429 воркер читает parameters.retry_after из тела ответа и откладывает повтор в нужной области лимита. Не обязательно замораживать всю очередь, если ограничение касается одного чата.

Контроль фактической доставки

Успешный ответ Bot API с объектом Message подтверждает успех вызова sendMessage, но не даёт квитанцию о прочтении и не исключает неопределённость при сетевом таймауте. Для критичного алерта нужно явное подтверждение человека или эскалация в другой канал.

Telegram Bot API не предоставляет информацию о прочтении сообщения обычным пользователем (только в каналах и супергруппах доступна статистика просмотров через отдельные методы). Косвенный контроль строят через периодические проверки активности подписки: раз в сутки или неделю бот отправляет тестовое сообщение с inline-кнопкой «Подписка активна» и ожидает нажатия в течение заданного времени. Отсутствие реакции на несколько тестовых сообщений подряд сигнализирует о проблеме и вызывает алерт через запасной канал.

Для критических уведомлений добавляют человеческое подтверждение: сообщение содержит кнопку «Принято» или «Реагирую», нажатие которой фиксируется системой. Отсутствие подтверждения в течение таймаута (например, 5 минут) запускает эскалацию: уведомление дублируется другому ответственному или отправляется через запасной канал.

Метрики мониторинга доставки

Метрика Что измеряет Пороговое значение
Время в очереди Задержка между записью события и началом отправки < 5 секунд для критических, < 30 секунд для остальных
Время доставки Задержка между записью события и получением 200 OK от API < 10 секунд для критических
Доля успешных отправок Процент сообщений, доставленных с первой попытки > 95%
Доля отброшенных Процент сообщений, не доставленных после всех повторов < 0.1%
Количество повторов Среднее число попыток на успешную доставку < 1.2 (большинство доставляется с первой попытки)
Скорость 429-ответов Частота превышения rate limit 0 (свидетельствует о корректной работе локального ограничителя)
Условный пример архитектуры
Условный пример архитектуры

Условный пример архитектуры

Представим систему мониторинга инфраструктуры, которая отправляет уведомления дежурной команде через Telegram-бота. Команда из пяти человек подписана на бота, каждый получает дубликат критических алертов для избыточности. Система генерирует до 20 событий в час в обычном режиме, до 200 в час при инциденте.

Компонент мониторинга при обнаружении превышения порога — например, утилизация CPU выше 90% в течение 3 минут — записывает событие в таблицу PostgreSQL:

INSERT INTO notification_queue (event_id, event_type, chat_ids, message, priority, status, created_at, attempts)
VALUES ('550e8400-e29b-41d4-a716-446655440000', 'cpu_high', '{123456, 234567, 345678, 456789, 567890}',
        'Критично: CPU сервера prod-web-01 превысил 90% (текущее: 94%)', 'high', 'pending', NOW(), 0);

Воркер на Python опрашивает таблицу каждые 2 секунды, выбирает записи со статусом pending, упорядоченные по приоритету и времени создания. Для каждой записи воркер обрабатывает список chat_id последовательно.

Перед отправкой проверяет Redis:

GET idempotency:550e8400-e29b-41d4-a716-446655440000:123456

Ключ отсутствует, воркер устанавливает статус «отправляется»:

SET idempotency:550e8400-e29b-41d4-a716-446655440000:123456 '{"status":"sending","ts":1696348800}' NX EX 3600

Вызывает Bot API:

POST https://api.telegram.org/bot<TOKEN>/sendMessage
{
  "chat_id": 123456,
  "text": "Критично: CPU сервера prod-web-01 превысил 90% (текущее: 94%)",
  "parse_mode": "Markdown"
}

Получает 200 OK, обновляет Redis:

SET idempotency:550e8400-e29b-41d4-a716-446655440000:123456 '{"status":"delivered","ts":1696348801}'

Повторяет для остальных четырёх chat_id. После обработки всех адресатов обновляет запись в PostgreSQL:

UPDATE notification_queue SET status = 'delivered', delivered_at = NOW() WHERE event_id = '550e8400-e29b-41d4-a716-446655440000';

При сетевом таймауте воркер не знает, принял ли Telegram первый запрос. Повтор sendMessage может создать дубль, потому что Bot API не принимает idempotency key для sendMessage. Практический выбор — at-least-once с редкими дублями или отказ от автоповтора на неопределённом таймауте с эскалацией в запасной канал. Кэш предотвращает параллельные повторы до вызова, но не устраняет эту сетевую неопределённость.

Поэтому результат такого условного примера нельзя описывать как «гарантированно без дублей». Измеряйте долю таймаутов, повторов и подтверждений, а для критичных алертов добавляйте человеческое подтверждение.

Безопасность и защита от злоупотреблений

Telegram-бот для служебных уведомлений — привилегированный канал, доступ к которому должен контролироваться. Три уровня защиты: ограничение подписки, защита webhook, контроль источников событий.

Подписка на бота требует явной авторизации. Пользователь отправляет команду /start токен_подписки, где токен сгенерирован системой и предоставлен пользователю через защищённый канал (например, веб-интерфейс под аутентификацией). Бот проверяет токен, связывает chat_id с учётной записью пользователя и сохраняет подписку. Без валидного токена подписка невозможна, что предотвращает спам и несанкционированный доступ.

Если бот получает обновления через webhook, используют параметр secret_token при настройке webhook. Telegram включает этот токен в заголовок X-Telegram-Bot-Api-Secret-Token каждого запроса к webhook. Приложение проверяет токен перед обработкой запроса, отклоняя подделки. Дополнительно webhook размещают на непредсказуемом URL-пути и ограничивают доступ по IP-адресам серверов Telegram (диапазоны публикуются в документации).

Компоненты системы, записывающие события в очередь уведомлений, должны иметь минимальные привилегии. Если мониторинговая система скомпрометирована, злоумышленник может генерировать ложные уведомления. Защита включает: аутентификацию при записи в очередь (API-ключ, внутренний токен), ограничение скорости записи на уровне источника (не более N событий в минуту от одного компонента), логирование всех записей для аудита.

Типичные ошибки реализации

Первая ошибка — синхронная отправка без очереди. Разработчик вызывает sendMessage напрямую в обработчике события, блокируя выполнение до завершения сетевого запроса. При задержке API или сбое основная логика приложения зависает, генерируется исключение, которое может прервать обработку транзакции. Решение: всегда разделять генерацию события и доставку через очередь.

Вторая ошибка — отсутствие анализа типа ошибки. Воркер повторяет отправку при любой ошибке, включая постоянные (блокировка бота пользователем, несуществующий chat_id). После нескольких попыток сообщение отбрасывается, но время и ресурсы потрачены впустую, очередь забита бесполезными повторами. Решение: различать временные и постоянные ошибки по HTTP-статусу и телу ответа, немедленно отбрасывать при постоянных.

Третья ошибка — игнорирование идемпотентности. Воркер повторяет отправку при таймауте, не проверяя, была ли доставлена первая попытка. Пользователь получает дубли, что снижает доверие к системе и создаёт путаницу при реагировании на инциденты. Решение: внедрить кэш отправленных сообщений и проверять его перед повтором.

Четвёртая ошибка — отсутствие мониторинга самой системы доставки. Разработчик считает, что раз воркер запущен, уведомления доставляются. На деле очередь может расти из-за превышения rate limit, воркер может упасть, Redis может быть недоступен. Решение: экспортировать метрики размера очереди, времени доставки, частоты ошибок, настроить алерты на аномалии.

Пятая ошибка — хранение токена бота в коде или публичном репозитории. Токен даёт полный контроль над ботом, включая отправку сообщений от его имени. Решение: хранить токен в переменных окружения, секретах оркестратора (Kubernetes Secrets, HashiCorp Vault), ограничивать доступ к ним.

Чек-лист внедрения
Чек-лист внедрения

Чек-лист внедрения

  • Создать бота через BotFather, получить токен, сохранить в защищённом хранилище.
  • Реализовать механизм подписки пользователей с авторизацией через токен или OAuth.
  • Спроектировать схему очереди сообщений: таблица БД или брокер сообщений, определить поля записи.
  • Разработать воркер для обработки очереди: извлечение событий, вызов Bot API, обработка ответа.
  • Встроить логику повторов с экспоненциальной задержкой и джиттером, ограничить максимальное число попыток.
  • Настроить Redis для кэша идемпотентности, реализовать проверку перед отправкой.
  • Внедрить локальный rate limiter (token bucket или скользящее окно) для соблюдения лимитов API.
  • Добавить обработку 429-ответов с чтением Retry-After.
  • Различать временные и постоянные ошибки, логировать постоянные для анализа.
  • Экспортировать метрики: размер очереди, время доставки, количество попыток, частота ошибок.
  • Настроить алерты на превышение пороговых значений метрик.
  • Реализовать запасной канал для критических уведомлений (email, SMS).
  • Провести нагрузочное тестирование: отправить пакет из 100 событий, проверить корректность доставки, отсутствие дублей, задержки.
  • Провести тестирование отказоустойчивости: имитировать сетевой сбой, недоступность Redis, падение воркера, проверить восстановление после перезапуска.
  • Документировать процедуры: подписка пользователей, эскалация при отказе доставки, ротация токена бота.

Частые вопросы

Можно ли обойтись без очереди для десятка сообщений в день?

Можно, если допустима потеря уведомления при сбое приложения в момент отправки и не требуется контроль доставки. Для служебных уведомлений это обычно неприемлемо: даже одно пропущенное сообщение о критическом инциденте может вызвать серьёзные последствия. Очередь добавляет надёжность при минимальных затратах на реализацию (таблица в существующей БД).

Как проверить, заблокировал ли пользователь бота?

Telegram Bot API не предоставляет метод для явной проверки блокировки. Блокировка обнаруживается при попытке отправки: метод sendMessage возвращает 400 с описанием «Forbidden: bot was blocked by the user». После этого подписку пользователя отмечают как неактивную, прекращают попытки отправки и уведомляют администратора через запасной канал.

Сколько хранить записи в очереди после доставки?

Для аудита и отладки полезно хранить записи несколько дней (типично 7–30 дней), затем удалять. Долгосрочное хранение истории уведомлений выносят в отдельное хранилище (архивная таблица, S3), чтобы не раздувать рабочую очередь и не замедлять запросы воркера.

Что делать, если очередь растёт быстрее, чем воркер обрабатывает?

Это сигнал о недостаточной пропускной способности. Решения: добавить параллельные воркеры (горизонтальное масштабирование), оптимизировать код воркера (батчинг запросов, если API поддерживает, хотя Telegram Bot API не поддерживает отправку нескольких сообщений одним запросом), проверить соблюдение rate limit (возможно, воркер тратит много времени на ожидание после 429). Временное решение — приоритизация: критические сообщения обрабатывать в первую очередь, второстепенные откладывать.

Нужен ли отдельный бот для каждого типа уведомлений?

Зависит от организации подписок. Если пользователи должны независимо подписываться на разные типы (например, один бот для алертов инфраструктуры, другой для уведомлений о завершении задач), удобнее создать несколько ботов. Если все получатели одной категории получают все уведомления этой категории, достаточно одного бота с разделением по chat_id.

Как обновить токен бота без простоя доставки?

Telegram Bot API позволяет отозвать токен и сгенерировать новый через BotFather командой /revoke. Старый токен перестаёт работать немедленно. Для бесшовной ротации: запустить второй воркер с новым токеном параллельно первому, дождаться обработки всех сообщений в очереди первым воркером (или дождаться истечения TTL событий), остановить первый воркер, отозвать старый токен. Альтернатива — кратковременный простой (несколько секунд): остановить воркер, обновить токен в конфигурации, перезапустить.

Заключение

Устойчивая доставка служебных уведомлений через Telegram требует разделения генерации события и отправки через очередь, повторных попыток с различением временных и постоянных ошибок, защиты от дублей через кэш идемпотентности и мониторинга всех стадий процесса. Telegram Bot API предоставляет простой интерфейс отправки, но не гарантирует доставку при сбоях — эту гарантию строит архитектура приложения. Внедрение описанных практик превращает бота из удобного, но ненадёжного канала в производственный инструмент для критических коммуникаций с контролируемой задержкой, отслеживаемыми ошибками и предсказуемым поведением при нагрузке.