Большинство ботов умирают не из-за плохой идеи, а из-за технических и продуктовых ошибок, которых легко было избежать. Собрали топ проблем, с которыми регулярно встречаемся в чужих проектах при аудите и поддержке. По каждой ошибке — симптом, последствия и как сделать правильно. Большинство этих проблем стоит копейки на этапе проектирования и десятки часов разбора, когда вылезают на пользователях.
Состояние FSM в памяти процесса
Самая частая архитектурная ошибка — хранить состояние диалога в обычном Python-словаре или JS-объекте.
# Плохо
user_states = {}
@dp.message()
async def handler(message):
user_states[message.from_user.id] = "waiting_phone"
Симптом: после рестарта процесса все активные диалоги ломаются, пользователи застревают посреди сценария. При запуске нескольких воркеров каждый видит свой кусок состояний.
Последствия: каждый деплой = массовый сброс активных сессий. Невозможно горизонтально масштабироваться. Утечка памяти при росте аудитории.
Как правильно — внешнее хранилище FSM (Redis, Postgres):
# Хорошо (aiogram 3)
from aiogram.fsm.storage.redis import RedisStorage
storage = RedisStorage.from_url("redis://localhost:6379/0")
dp = Dispatcher(storage=storage)
У grammY — @grammyjs/storage-redis, у teloxide — RedisStorage. Это базовый минимум для прода.
Polling в production с несколькими инстансами
getUpdates (long polling) предполагает ровно один потребитель. Если запустить два инстанса бота с одним токеном — оба получают разные апдейты, между ними начинаются конфликты.
Симптом: в логах ошибки Conflict: terminated by other getUpdates request, часть сообщений теряется, часть обрабатывается дважды.
Последствия: пользователи получают рассинхронизированные ответы, заказы дублируются или пропадают.
Как правильно: для прода — webhook (Telegram сам шлёт апдейты на ваш HTTPS-эндпоинт, балансировка через nginx или k8s service). Polling оставьте для локальной разработки.
Webhook без secret_token
Webhook, доступный по URL без проверки подписи — открытая дверь. Любой, кто узнал ваш URL (а узнать его несложно — он часто светится в логах CDN), может слать поддельные апдейты.
# Хорошо
await bot.set_webhook(
url="https://example.com/tg/webhook",
secret_token=os.environ["TG_WEBHOOK_SECRET"],
)
# В обработчике
async def webhook(request):
if request.headers.get("X-Telegram-Bot-Api-Secret-Token") != SECRET:
return web.Response(status=403)
Симптом: всплески странных апдейтов с несуществующими chat_id, попытки эксплуатировать команды от имени админа.
Последствия: спуфинг команд, обход бизнес-логики, в худшем случае — несанкционированные действия в БД.
Как правильно: всегда задавать secret_token в setWebhook и проверять заголовок X-Telegram-Bot-Api-Secret-Token на каждом запросе.
Не обработан FloodWait и 429
Telegram ограничивает: примерно 30 сообщений в секунду на бота, 1 сообщение в секунду на чат, 20 сообщений в минуту на группу. Если ломиться сильнее — приходит 429 с полем retry_after.
Симптом: в логах массовые Too Many Requests: retry after N, рассылка частично теряется.
Последствия: при упорном игноре — временный бан бота на стороне Telegram (несколько часов), часть сообщений не доходит до пользователей.
Как правильно — exponential backoff и очередь с лимитером:
# Хорошо
async def send_with_retry(chat_id, text, max_retries=5):
for attempt in range(max_retries):
try:
return await bot.send_message(chat_id, text)
except TelegramRetryAfter as e:
await asyncio.sleep(e.retry_after + 1)
except TelegramForbiddenError:
await mark_user_blocked(chat_id)
return
Для рассылок — отдельный воркер с rate limit 25 msg/sec, не пытаться слать всё параллельно.
Synchronous тяжёлые вызовы прямо в handler
LLM-запрос на 10 секунд, SQL на 5 секунд, генерация PDF на 8 секунд — внутри обработчика апдейта.
Симптом: webhook отвечает медленно, очередь апдейтов растёт, Telegram повторяет запросы (он ждёт ответ до 60 секунд), пользователь получает дубли.
Последствия: лавинообразная деградация под нагрузкой, потерянные апдейты, дубликаты сообщений.
Как правильно: webhook отвечает 200 за 1–2 секунды, тяжёлая работа уходит в очередь (Celery/RQ для Python, BullMQ для Node, NATS/Asynq для Go). Воркер обрабатывает задачу и сам шлёт результат через sendMessage.
Не обработан 403 «бот заблокирован»
Когда пользователь нажал «Заблокировать бота», любой sendMessage на его chat_id возвращает 403 Forbidden: bot was blocked by the user.
Симптом: при рассылках — массовые ошибки 403, забитые логи, лишний трафик к Bot API.
Последствия: рассылка тормозит, метрики «доставлено» врут, в Postgres растёт мёртвый сегмент пользователей.
Как правильно: ловим TelegramForbiddenError, помечаем пользователя is_blocked=true и blocked_at=now(), исключаем из последующих рассылок. Раз в неделю можно пытаться писать снова — пользователь мог разблокировать.
callback_data длиннее 64 байт
Telegram ограничивает callback_data 64 байтами. Если пытаться запихать туда JSON с десятком полей — кнопка просто не создастся, либо обрежется.
# Плохо — может не влезть
callback_data = json.dumps({"action": "buy", "product_id": 123, "user": 456, "promo": "BLACK_FRIDAY_2025"})
# Хорошо — короткий ID, остальное в Redis
ticket_id = secrets.token_urlsafe(8)
await redis.setex(f"cb:{ticket_id}", 3600, json.dumps(payload))
button = InlineKeyboardButton(text="Купить", callback_data=f"buy:{ticket_id}")
Симптом: BUTTON_DATA_INVALID или MESSAGE_NOT_MODIFIED, кнопки не работают для длинных payload.
Как правильно: класть в callback_data короткий ID, полные данные хранить в Redis с TTL.
Параллельная обработка одного update_id
Если webhook вызвался дважды (Telegram дублировал из-за таймаута), оба вызова идут в обработку — заказ создаётся дважды.
Симптом: жалобы «у меня списали два раза», в БД дубликаты заказов с разницей в секунду.
Последствия: финансовые потери, ручные возвраты, испорченная репутация.
Как правильно — идемпотентность по update_id:
# Хорошо
async def handle_update(update):
acquired = await redis.set(
f"update:{update.update_id}", "1",
ex=600, nx=True,
)
if not acquired:
return # уже обработано
await process_update(update)
Для платежей — отдельный idempotency_key на уровне pre_checkout_query и successful_payment.
Токен бота в коде или в git
Токен закоммичен в config.py, в .env.example или в Dockerfile. Спустя месяц репозиторий стал публичным, или один из разработчиков унёс ноут.
Симптом: внезапно бот шлёт сообщения, которых вы не писали, либо удалён через deleteWebhook.
Последствия: компрометация всех пользователей, кража FSM-состояний, спам от вашего имени, отзыв токена через BotFather и потеря username (если повезёт — вернуть успеете).
Как правильно: токен только в переменных окружения или в секретах (Vault, AWS Secrets Manager, Doppler). В git — только .env.example с пустыми значениями. Pre-commit хук с gitleaks или trufflehog.
Нет rate limit per user
Один тролль с автокликером шлёт 100 команд в секунду — бот пытается всё обработать, очередь забивается, остальные пользователи ждут.
Симптом: пиковые нагрузки от одного user_id, остальные жалуются на таймауты.
Как правильно — лимит на уровне приложения:
# Хорошо (aiogram middleware)
async def __call__(self, handler, event, data):
user_id = event.from_user.id
key = f"rl:{user_id}"
count = await redis.incr(key)
if count == 1:
await redis.expire(key, 60)
if count > 30:
return # тихо игнорируем
return await handler(event, data)
Стандарт — 20–30 сообщений в минуту на пользователя, для админов — выше.
Не валидируется WebApp initData
Mini App шлёт initData на бэкенд, бэкенд берёт user_id без проверки HMAC.
Симптом: внешне всё работает, но любой может через DevTools подменить initDataUnsafe.user.id и зайти от имени другого.
Последствия: критическая дыра в авторизации, доступ к чужим данным, к чужим балансам.
Как правильно: проверять HMAC-SHA256 по алгоритму из доков Telegram, плюс auth_date не старше нескольких часов. Подробно — в нашем посте про initData.
Жёстко зашитые тексты вместо локализации
Все строки прямо в коде: "Здравствуйте, выберите товар". Через полгода нужно добавить английский — приходится перекапывать всё кодовую базу.
Как правильно: с первого дня — словарь локализаций (gettext, i18next, fluent). Даже если язык один, ключи проще менять централизованно.
# Хорошо
await message.answer(_("greeting.welcome"))
Бонус: маркетинг сможет править копирайт без релиза, если вынести строки в Redis или в админку.
Нет fallback при отправке в группу
Бот добавлен в группу без прав на отправку, либо администратор временно лишил его прав. Любой sendMessage падает с Bad Request: not enough rights to send messages.
Симптом: бот молча ломается на конкретных чатах, пользователи в группе не получают уведомлений.
Как правильно: ловить TelegramBadRequest и TelegramForbiddenError, помечать чат как «нет прав», уведомлять администратора группы в личке (если он давал согласие) или отключать функционал для этого чата.
Глобальный try/except, который проглатывает всё
# Плохо
try:
await process(update)
except Exception:
pass
Симптом: бот «работает», но молча роняет половину запросов. Узнаёте об этом из жалоб через неделю.
Как правильно: логировать всё с трейсбэком, отправлять в Sentry, разделять «ожидаемые» исключения (TelegramRetryAfter, TelegramForbiddenError) и «неожиданные» (баги — должны приводить к алерту).
# Хорошо
try:
await process(update)
except TelegramRetryAfter as e:
await asyncio.sleep(e.retry_after)
except TelegramForbiddenError:
await mark_blocked(update)
except Exception:
logger.exception("unexpected handler error")
sentry_sdk.capture_exception()
Прямые SQL-запросы без параметризации
# Плохо
await db.execute(f"SELECT * FROM orders WHERE user_id = {user_id}")
Если user_id пришёл из callback_data или текста — SQL injection.
Как правильно: параметризованные запросы ($1, ?, named params) или ORM (SQLAlchemy, Tortoise, Prisma).
# Хорошо
await db.execute("SELECT * FROM orders WHERE user_id = $1", user_id)
Нет индексов в БД
На старте 100 пользователей, всё летает. Через полгода — 200 тысяч, и SELECT WHERE telegram_id = ? идёт за 800 мс через seq scan.
Симптом: латентность handler'ов плавно растёт, SQL-логи показывают долгие запросы.
Как правильно: индексы на telegram_id, chat_id, created_at (если фильтруете по дате), составные индексы для популярных WHERE-комбинаций. EXPLAIN ANALYZE на топовых запросах раз в квартал.
Нет graceful shutdown
При деплое контейнер получает SIGTERM, процесс умирает мгновенно, незавершённые обработчики обрываются.
Симптом: после каждого деплоя — несколько потерянных платежей или зависших FSM.
Как правильно — обрабатывать сигналы, дожидаться текущих задач:
# Хорошо
async def main():
stop = asyncio.Event()
loop = asyncio.get_running_loop()
for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(sig, stop.set)
polling_task = asyncio.create_task(dp.start_polling(bot))
await stop.wait()
await dp.stop_polling()
await polling_task
await bot.session.close()
Для k8s — terminationGracePeriodSeconds: 30, чтобы успеть доесть очередь.
Сразу в прод без staging
«Тестировать будем на пользователях». Через два часа после релиза — массовые жалобы, откат.
Как правильно: отдельный бот @yourapp_staging с отдельной БД и отдельным токеном. Любой релиз сначала туда, прогон smoke-тестов, потом в прод. CI отдельный для каждой среды.
Нет мониторинга и алертов
Бот молча зависает в три часа ночи. Узнаёте утром из чата техподдержки.
Как правильно:
/healthendpoint, проверяет БД и Redis.- Внешний пинг (UptimeRobot, Healthchecks.io) каждые 60 секунд, алерт в Telegram при первом провале.
- Метрики (Prometheus + Grafana или простой statsd): количество апдейтов в секунду, латентность handler'ов, очередь задач.
- Sentry на исключения.
Нет проверенных бэкапов
Бэкап БД настроен, складывается в S3. Однажды БД ломается, идёте восстанавливать — оказывается, последний валидный дамп месячной давности.
Как правильно: автоматический бэкап ежедневно + проверка восстановления раз в месяц на отдельной машине. Принцип «бэкап без проверки восстановления — не бэкап».
Игнор 152-ФЗ
Бот собирает имя, телефон, email пользователей, но нигде нет политики обработки персональных данных, нет согласия, нет уведомления Роскомнадзора.
Последствия: штрафы РКН (до 18 млн ₽ для юрлиц после поправок 2025 года), блокировка ресурса.
Как правильно: политика обработки ПДн на сайте, явное согласие в боте перед сбором (отдельная кнопка), уведомление РКН об обработке ПДн, при использовании Telegram — уведомление о трансграничной передаче.
Webhook на нестандартном пути без аутентификации
URL https://example.com/webhook — кто угодно может ddos'ить ваш handler пустыми POST'ами.
Как правильно: путь с секретом (/tg/webhook/<random_token>) + проверка X-Telegram-Bot-Api-Secret-Token + rate limit на nginx. IP whitelist Telegram (диапазоны опубликованы) — для параноидальных случаев.
Слишком короткий или вечный TTL у FSM
Если TTL FSM 5 минут — пользователь, отвлёкшийся на звонок, возвращается и видит «Не понимаю» вместо продолжения. Если TTL вечный — забытые сценарии плодятся в Redis годами.
Как правильно: TTL 1–7 дней для большинства сценариев, активный сброс по /cancel и при завершении сценария.
Хранение паролей пользователей
Бот придумывает свою регистрацию: логин, пароль, хеш в БД. Зачем — непонятно. Telegram уже аутентифицировал пользователя через user_id.
Как правильно: пользователь = telegram_id. Никаких паролей, никаких email-подтверждений, никаких «забыли пароль». Это главная фишка ботов.
Миграции БД руками
Разработчик локально дописал колонку через ALTER TABLE, в проде колонки нет, релиз падает.
Как правильно: миграции через инструмент (Alembic для Python, dbmate, Flyway, Prisma Migrate). Каждое изменение схемы — отдельная миграция в git, применяется CI на каждой среде. Никаких ручных ALTER TABLE на проде.
Рассылки в нерабочее время
Маркетинг запустил рассылку в 4 утра «по всей базе». Половина пользователей разбужена пушем, нажимает «Заблокировать бота».
Как правильно: учитывать language_code пользователя для приблизительной таймзоны или явно собирать таймзону в профиле. Окно отправки — 10:00–21:00 по локальному времени. Опция «не беспокоить» в настройках.
В Mini App не проверяются themeParams
Mini App вёрстана под светлую тему. Пользователь с тёмной темой Telegram видит чёрный текст на чёрном фоне.
Как правильно: использовать Telegram.WebApp.themeParams или CSS-переменные --tg-theme-*:
/* Хорошо */
body {
background: var(--tg-theme-bg-color, #ffffff);
color: var(--tg-theme-text-color, #000000);
}
.button {
background: var(--tg-theme-button-color, #2481cc);
color: var(--tg-theme-button-text-color, #ffffff);
}
Плюс подписка на themeChanged — пользователь может переключить тему прямо в Mini App.
Итого
Топ ошибок при разработке Telegram-бота: FSM в памяти процесса, polling в проде вместо webhook, webhook без secret_token, игнор FloodWait и 403, синхронные тяжёлые вызовы в handler, длинный callback_data, отсутствие идемпотентности по update_id, токен в git, нет rate limit per user, не проверяется initData Mini App, отсутствие локализации, проглатывание ошибок в глобальном try/except, SQL injection и отсутствие индексов, нет graceful shutdown и мониторинга, игнор 152-ФЗ. Большинство решается на этапе проектирования и стоит копейки. Когда вылезает на пользователях — стоит на порядки дороже. Хорошая архитектура — лучшая инвестиция в долгую жизнь бота.
Частые вопросы
Почему нельзя хранить FSM в памяти процесса бота?
Хранение состояний в обычном словаре или объекте кажется простым, но при первом же рестарте все активные диалоги ломаются — пользователи застревают посреди сценария. При запуске нескольких воркеров каждый видит только свой кусок состояний, и поведение становится недетерминированным. Память растёт линейно с аудиторией, утечки неизбежны. Правильно — внешнее хранилище FSM: Redis, Postgres или MongoDB. У aiogram это RedisStorage, у grammY — @grammyjs/storage-*, у teloxide — RedisStorage. Это базовый минимум для любого продакшен-бота, без него после первого деплоя теряются все активные диалоги.
Как защитить webhook Telegram-бота от подделки и DDoS?
Webhook без проверки подписи — открытая дверь, любой может слать поддельные апдейты. Минимум: при setWebhook задать secret_token, на каждом запросе проверять заголовок X-Telegram-Bot-Api-Secret-Token и отвечать 403 если не совпадает. Слушать только HTTPS. Использовать длинный случайный путь вместо /webhook, чтобы URL не угадывался. Включить rate limit на nginx. Для параноидальных случаев — IP whitelist по диапазонам Telegram. Без этих мер handler можно ddos-ить пустыми POST'ами или спуфить команды от имени админа.
Почему нельзя делать тяжёлые вызовы прямо в webhook handler?
Telegram повторяет запрос, если webhook не ответил за 60 секунд — пользователь получает дубли. LLM-запрос на 10 секунд, SQL на 5 секунд, генерация PDF на 8 секунд внутри handler приводят к деградации под нагрузкой и потере апдейтов. Правильная архитектура: webhook отвечает 200 за 1–2 секунды, тяжёлые задачи кладутся в очередь (Celery/RQ для Python, BullMQ для Node, NATS/Asynq для Go), отдельный воркер обрабатывает задачу и сам шлёт результат через sendMessage. Это же даёт устойчивость к падениям воркера и возможность масштабироваться.
Как правильно обрабатывать ошибки Bot API: 403, 429, 500?
Bot API возвращает несколько классов ошибок, и каждый требует своей логики. 403 — пользователь заблокировал бота: ловим TelegramForbiddenError, помечаем is_blocked=true в БД, исключаем из рассылок. 429 (Too Many Requests, FloodWait) — делаем sleep на retry_after секунд и повторяем; для рассылок добавляем rate limiter на 25 msg/sec. 500 — повторяем с exponential backoff, не считаем сообщение отправленным. 400 — логируем как баг, не повторяем (плохой запрос не починится). Глобальный try/except: pass — антипаттерн, всё валится в Sentry.
Как обеспечить идемпотентность в Telegram-боте?
Telegram может прислать один update_id дважды (если webhook не ответил вовремя или сеть моргнула). Без идемпотентности заказ создаётся два раза, платёж списывается дважды. Решение: на входе webhook делаем redis.set(f"update:id", "1", ex=600, nx=True) — если ключ уже существует, выходим. Для платежей дополнительно — уникальный idempotency_key на pre_checkout_query и successful_payment, БД должна иметь UNIQUE-индекс на этот ключ. callback_data длиннее 64 байт также вызывает проблемы — храните полный payload в Redis, в callback_data кладите короткий ID.
Какие основные ошибки безопасности при разработке Telegram-бота?
Топ дыр в безопасности: токен бота в git или в коде вместо переменных окружения (компрометация = потеря бота); webhook без secret_token (спуфинг команд); прямые SQL-запросы с подстановкой переменных вместо параметризации (SQL injection); отсутствие проверки HMAC у Mini App initData (подмена user_id через DevTools); нет rate limit per user (один тролль кладёт бот); хранение паролей пользователей вместо доверия Telegram-аутентификации; игнор 152-ФЗ при сборе ПДн (штрафы РКН до 18 млн ₽). Все эти проблемы решаются стандартными практиками — секрет-менеджер, ORM, HMAC-валидация, политика ПДн.
Какие операционные ошибки приводят к простоям Telegram-бота?
Главные операционные грабли: нет graceful shutdown (SIGTERM убивает процесс мгновенно, FSM теряются); нет /health endpoint и внешнего мониторинга (зависание замечают через жалобы); нет staging-окружения с отдельным ботом и БД (баги летят в прод); миграции БД руками вместо alembic/dbmate/flyway (drift между средами); бэкап без проверки восстановления (когда нужно — оказывается невалидным); нет индексов в БД (запросы тормозят с ростом базы); рассылки в нерабочее время местного пользователя (массовые блокировки бота). Каждый пункт — несколько часов настройки на старте против дней разбора потом.