Как я к этому пришёл
Слово async перед функцией не делает код асинхронным — оно лишь разрешает внутри await. Если внутри вызвать блокирующую операцию, цикл событий встанет целиком: пока эта строка выполняется, ни один другой пользователь не будет обслужен.
Именно это у меня и происходило. Я написал сотню асинхронных обработчиков и один синхронный запрос к базе в функции построения отчёта. На маленькой выборке разница была незаметна, на годовой — секунды полной тишины.
Диагностика оказалась поучительной: я добавил замер времени вокруг каждого await и увидел, что цикл событий простаивает ровно там, где ждать не должен.
Отдельный урок — деплой. Пока бот жил только у меня, «работает на моей машине» устраивало. Как только понадобилось поднять его на сервере, выяснилось, что версия Python другая, а зависимости конфликтуют. docker-compose снял вопрос: одна команда — и окружение идентично.
Бот работает каждый день, отчёт за месяц строится меньше чем за секунду, а развёртывание на новом сервере занимает одну команду.