FastAPI и aiogram: совместный запуск веб-сервера и Telegram-бота

Ошибка RuntimeError: This event loop is already running при попытке запустить aiogram-бота внутри FastAPI-приложения — самый частый признак того, что поллинг бота и ASGI-сервер запущены как два независимых цикла событий. Проблема решается не отключением одного из компонентов, а правильной архитектурой: либо бот работает через вебхуки и принимает обновления прямо в эндпоинт FastAPI, либо поллинг запускается как фоновая задача в общем event loop через событие startup.

Связка FastAPI и aiogram встречается в проектах, где Telegram-бот — лишь один из интерфейсов системы: рядом нужны REST API для мобильного клиента, админка, приём платежных уведомлений или интеграция с внешними сервисами. Оба фреймворка асинхронные и построены на asyncio, поэтому они отлично уживаются в одном процессе — если не нарушать несколько базовых правил, о которых пойдёт речь ниже.

Зачем объединять FastAPI и aiogram в одном приложении

Типовой сценарий: бот должен реагировать на события, которые приходят не из Telegram, а извне — например, отправить пользователю уведомление после успешной оплаты или после изменения статуса заказа в CRM. Если бот и API живут в разных процессах, приходится строить обмен через очереди сообщений или прямые HTTP-вызовы между сервисами. В одном процессе всё проще: эндпоинт FastAPI напрямую вызывает bot.send_message(), потому что объект бота доступен в той же памяти.

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

  • 🚀 Единый процесс — проще деплой и мониторинг, не нужна межсервисная шина.
  • 🗄️ Общий пул подключений к базе данных и единые модели данных.
  • 🔔 API может инициировать отправку сообщений в Telegram без промежуточных очередей.
  • ⚙️ Одна конфигурация и один набор секретов вместо двух синхронизируемых копий.

Два подхода: вебхуки против поллинга в фоне

Первый подход — webhook: Telegram сам отправляет обновления POST-запросами на указанный URL, и FastAPI принимает их в обычном эндпоинте. Это «правильный» способ для продакшена: нет постоянного опроса серверов Telegram, обновления приходят мгновенно, а нагрузка масштабируется вместе с веб-сервером. Минус — нужен публичный HTTPS-адрес с валидным сертификатом, что усложняет локальную разработку (обычно выручают туннели вроде ngrok).

Второй подход — запуск поллинга как фоновой задачи внутри того же event loop, где работает FastAPI. Aiogram-поллинг стартует через asyncio.create_task() в обработчике события запуска приложения, и оба компонента делят один цикл событий. Такой вариант удобен для разработки и небольших проектов: не нужен публичный адрес, достаточно запустить uvicorn локально.

КритерийВебхукПоллинг в фоне
Требования к сетиПубличный HTTPS-доменТолько исходящий доступ в интернет
Задержка доставкиМинимальная, push-модельЗависит от интервала опроса
Локальная разработкаНужен туннель (ngrok и аналоги)Работает из коробки
МасштабированиеНесколько воркеров за балансировщикомОдин процесс — один поллинг
📊 Какой способ интеграции FastAPI и aiogram вы используете?
Вебхуки в продакшене
Поллинг как фоновая задача
Пока только изучаю варианты
Разделяю бота и API на два сервиса

Настройка вебхука: пошаговая схема

Суть интеграции: при старте приложения бот регистрирует вебхук через метод set_webhook(), а FastAPI получает обновления в POST-эндпоинте и передаёт их диспетчеру aiogram. В aiogram 3.x для этого используется метод dp.feed_webhook_update(), который принимает объект бота и разобранное обновление. Путь эндпоинта стоит делать неочевидным — например, включать в него токен или случайную строку, чтобы посторонние не могли подделывать обновления.

from fastapi import FastAPI, Request

from aiogram import Bot, Dispatcher

from aiogram.types import Update

app = FastAPI()

bot = Bot(token=BOT_TOKEN)

dp = Dispatcher()

@app.post("/webhook/{secret}")

async def telegram_webhook(secret: str, request: Request):

if secret != WEBHOOK_SECRET:

return {"ok": False}

update = Update.model_validate(await request.json())

await dp.feed_webhook_update(bot, update)

return {"ok": True}

@app.on_event("startup")

async def on_startup():

await bot.set_webhook(f"{BASE_URL}/webhook/{WEBHOOK_SECRET}")

@app.on_event("shutdown")

async def on_shutdown():

await bot.delete_webhook()

await bot.session.close()

☑️ Проверка перед запуском вебхука

Выполнено: 0 / 5
⚠️ Внимание: если вебхук зарегистрирован, поллинг для того же бота работать не будет — Telegram перестаёт отдавать обновления через getUpdates. Перед переключением режимов удаляйте вебхук вызовом delete_webhook(), иначе бот «замолчит» без видимых ошибок в логах.

Поллинг как фоновая задача внутри FastAPI

Для сценария без публичного адреса поллинг запускается в событии startup через asyncio.create_task(). Критично не вызывать dp.start_polling() блокирующе и не создавать отдельный цикл событий через asyncio.run() — именно эти два действия порождают конфликт циклов, упомянутый в начале статьи. Задача должна жить в том же loop, что и uvicorn.

@app.on_event("startup")

async def on_startup():

asyncio.create_task(dp.start_polling(bot))

@app.on_event("shutdown")

async def on_shutdown():

await dp.stop_polling()

await bot.session.close()

Обратите внимание на остановку: при завершении приложения нужно корректно остановить диспетчер и закрыть сессию бота, иначе в логах появятся предупреждения о незакрытых соединениях, а в худшем случае процесс будет «висеть» при перезапуске. Если используете несколько воркеров uvicorn (--workers 2 и больше), поллинг запустится в каждом из них, и Telegram будет отдавать обновления только одному процессу — остальные останутся без сообщений. Для поллинга держите один воркер.

Отправка сообщений из эндпоинтов FastAPI

Главное преимущество совместного запуска раскрывается, когда HTTP-API должен что-то сообщить в Telegram. Поскольку объект bot живёт в том же процессе, любой эндпоинт может напрямую вызвать bot.send_message() — например, после обработки колбека платёжной системы. Важно лишь хранить chat_id пользователей в базе и не пытаться держать их в памяти: при перезапуске процесса вся оперативная память обнуляется.

Ещё один нюанс — обработка ошибок Telegram API. Если пользователь заблокировал бота, вызов send_message() выбросит исключение TelegramForbiddenError. Такие случаи стоит перехватывать, помечать пользователя неактивным в базе и не ронять весь HTTP-запрос из-за недоставленного уведомления.

Как организовать доступ к объекту bot в роутерах

Удобный паттерн — сохранить bot в app.state при старте (app.state.bot = bot) и получать его в эндпоинтах через request.app.state.bot. Альтернатива — dependency injection через Depends с функцией-провайдером, что удобнее для тестирования.

Структура проекта и общие ресурсы

Чтобы код не превратился в клубок зависимостей, разделяйте слои: пакет bot/ с хендлерами и роутерами aiogram, пакет api/ с роутерами FastAPI, общий пакет db/ с моделями и сессиями базы данных. Точка входа собирает всё вместе: создаёт приложение FastAPI, подключает роутеры обеих сторон и регистрирует обработчики жизненного цикла.

  • 📁 bot/handlers/ — хендлеры команд и сообщений, регистрируемые в Dispatcher.
  • 📁 api/routes/ — HTTP-эндпоинты, включая эндпоинт вебхука.
  • 📁 db/ — engine, фабрика сессий и модели SQLAlchemy, общие для обеих сторон.
  • 📁 config.py — настройки через pydantic-settings из переменных окружения.
⚠️ Внимание: не создавайте отдельное подключение к базе данных для бота и отдельное для API без крайней необходимости. Один асинхронный engine с общим пулом соединений предсказуемее: иначе при росте нагрузки можно упереться в лимит подключений PostgreSQL, и диагностировать причину будет непросто.

Типичные ошибки и их диагностика

Первая группа проблем связана с циклом событий: RuntimeError о уже запущенном loop, зависание приложения при старте, молчание бота без ошибок. Проверьте, что нигде в коде не вызывается asyncio.run() после старта uvicorn и что поллинг запущен именно через create_task(), а не как блокирующий вызов в startup — блокирующий вызов не даст веб-серверу начать принимать запросы.

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

Третья группа — логические: хендлеры не срабатывают, хотя обновления доходят. Убедитесь, что роутеры подключены к диспетчеру (dp.include_router()) до начала приёма обновлений, а фильтры хендлеров не пересекаются так, что один перехватывает сообщения раньше другого.

Часто задаваемые вопросы

Можно ли запускать FastAPI и aiogram в разных процессах?

Да, и для крупных проектов это даже предпочтительнее. Тогда бот и API общаются через базу данных, брокер сообщений (Redis, RabbitMQ) или внутренние HTTP-запросы. Совместный запуск в одном процессе оправдан для небольших и средних проектов, где важна простота деплоя.

Что выбрать для продакшена: вебхук или поллинг?

Для продакшена обычно выбирают вебхук: мгновенная доставка, меньше лишнего трафика, возможность горизонтального масштабирования. Поллинг оправдан, когда нет публичного HTTPS-адреса или проект небольшой и запускается в один воркер.

Почему бот молчит после регистрации вебхука?

Проверьте три вещи: не запущен ли параллельно поллинг (при активном вебхуке getUpdates не работает), что отвечает getWebhookInfo (там видны ошибки доставки) и доходит ли POST-запрос до вашего эндпоинта — посмотрите access-логи веб-сервера.

Как версии aiogram влияют на интеграцию?

Примеры в статье ориентированы на aiogram 3.x, где используются feed_webhook_update() и dp.start_polling(). В aiogram 2.x API отличается (например, process_update и executor.start_polling), поэтому при работе со второй версией сверяйтесь с документацией именно вашей версии — прямой перенос кода не сработает.

Нужен ли Redis или другие внешние сервисы для связки?

Нет, для базовой интеграции достаточно FastAPI, aiogram и базы данных. Redis становится нужен, когда требуются FSM-хранилище вне памяти процесса, фоновые задачи через очередь или работа нескольких экземпляров приложения с общим состоянием.