Ошибка 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 и аналоги) | Работает из коробки |
| Масштабирование | Несколько воркеров за балансировщиком | Один процесс — один поллинг |
Настройка вебхука: пошаговая схема
Суть интеграции: при старте приложения бот регистрирует вебхук через метод 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()
☑️ Проверка перед запуском вебхука
⚠️ Внимание: если вебхук зарегистрирован, поллинг для того же бота работать не будет — 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-хранилище вне памяти процесса, фоновые задачи через очередь или работа нескольких экземпляров приложения с общим состоянием.