// Python Dev
Два дня на задачу, которая казалась тривиальной: асинхронность загрузки в Telegram-ботах
Опубликовано 30.05.2026
Есть класс задач, которые выглядят как пятнадцать минут работы. Потом садишься за них и обнаруживаешь, что дело не в коде — дело в том, как устроена система под капотом. Это история про одну такую задачу.
Контекст
Я разрабатывал Telegram-бота с несложной на первый взгляд логикой: пользователь отправляет фотографии, бот их собирает, ждёт последнюю и спрашивает — «Есть ли ещё?». Есть ограничения: минимум 5 фото, максимум 30. Если меньше пяти — молчим, ждём ещё. Если пришло больше тридцати — останавливаем и говорим, что хватит. Дальше FSM переходит в следующее состояние и начинается основная обработка.
Казалось бы: ловим событие, считаем фото, после последнего шлём кнопку. Пара часов работы с чтением спецификаций и тестами, максимум.
Но что-то пошло не так и работа затянулась на два дня.
Как Telegram работает с фотографиями
Первое, что я не учёл: Telegram не передаёт фотографии по одной, если их несколько. Он группирует их в группы, присваивая каждой media_group_id — и отправляет чанками по 10 штук. Это поведение встроено в платформу и не настраивается.
Казалось бы, ладно: ловим события по media_group_id, дожидаемся тишины, отправляем кнопку. Но здесь второй сюрприз.
Telegram заранее не сообщает, сколько фотографий будет в группе.
Ты получаешь события одно за другим. Каждое следующее может быть последним — а может, нет. Если фотографии большие, между событиями возникают паузы: серверы Telegram обрабатывают загрузку. И ты стоишь перед классической проблемой неопределённости: ждать дальше или уже отправлять сообщение?
Первые попытки
Первая идея — ждать фиксированный таймаут после каждого события. Подождали 1-2 секунды тишины — значит, всё пришло, шлём кнопку. Логика понятная, реализуется просто: отслеживаем время последнего полученного фото, запускаем фоновую проверку без блокировки основного потока. Но вы же помните, что фото могут быть большими, сети не стабильными и ожидание превращается в лотерею.
Проблема с таймстэмпами
Вот что происходит на практике. Пользователь отправляет 25 фотографий. Клиент начинает загрузку. Сервера Telegram принимают фото и обрабатывают их — и именно в момент обработки на сервере фиксируется таймстэмп события, а не момент отправки с устройства.
Это значит: события sendMediaGroup могут приходить в бот с задержкой и в непредсказуемом порядке относительно того, как они видны в интерфейсе клиента.
Мы ловим последнее событие, ждём секунду, шлём сообщение с кнопкой. Всё логично. Но в интерфейсе пользователя это выглядит так: сначала появляется наша кнопка, потом — бах — поверх неё приходят ещё несколько фотографий, которые Telegram показывает в таймлайне по времени своей обработки.
Кнопка улетает вверх. Пользователь её не видит и не понимает, что делать дальше.
Я пробовал разные таймауты, следил за номерами входящих сообщений — ничего не давало стабильного результата. Потому что проблема не в таймауте. Проблема в том, что ты не контролируешь порядок появления сообщений в интерфейсе клиента: это решает Telegram на своей стороне.
Решение, которое сработало
В итоге я пришёл к простой идее, которая решает проблему не предотвращением, а реакцией на факт.
Алгоритм такой:
После последнего полученного события ждём 1-2 секунды. Отправляем сообщение с кнопкой и сохраняем его message_id. Если после этого прилетает ещё одно фото — удаляем старое сообщение с кнопкой и отправляем новое. Повторяем, пока не наступит реальная тишина.
# Примерно так это выглядит на aiogram
last_photo_time = {}
button_message_id = {}
async def handle_photo(message: Message, state: FSMContext):
user_id = message.from_user.id
last_photo_time[user_id] = time.time()
data = await state.get_data()
photos = data.get("photos", [])
photos.append(message.photo[-1].file_id)
await state.update_data(photos=photos)
# Удаляем предыдущую кнопку, если она была
if user_id in button_message_id:
try:
await message.bot.delete_message(
chat_id=message.chat.id,
message_id=button_message_id[user_id]
)
except Exception:
pass
# Ждём тишины без блокировки
asyncio.create_task(check_and_send_button(message, state))
async def check_and_send_button(message: Message, state: FSMContext):
user_id = message.from_user.id
await asyncio.sleep(1.5)
# Если за это время пришло новое фото — выходим, новая задача уже запущена
if time.time() - last_photo_time[user_id] < 1.4:
return
data = await state.get_data()
photos = data.get("photos", [])
if len(photos) < 5:
return # Молчим, ждём ещё
if len(photos) >= 30:
# Говорим, что хватит, переходим дальше
await state.set_state(NextState.processing)
return
# Отправляем кнопку и запоминаем id
sent = await message.answer(
"Это все фотографии?",
reply_markup=confirmation_keyboard()
)
button_message_id[user_id] = sent.message_id
Ключевая идея: мы не пытаемся угадать, когда придёт последнее фото. Мы просто готовы переотправить кнопку, если ошиблись. Пользователь видит актуальную кнопку всегда внизу ленты, рядом с последними фото. Визуально пропадающая кнопка не заметна.
Что здесь важно с точки зрения архитектуры
Это хороший пример того, что асинхронные системы часто нельзя проектировать исходя из предположений о порядке событий. Telegram — асинхронная система со своей логикой обработки на серверах. Наш бот — тоже асинхронная система. Когда они взаимодействуют, порядок событий в обоих интерфейсах не гарантирован.
Первый инстинкт — предсказать поведение и написать под него код. Второй, более устойчивый инстинкт — сделать систему, которая корректно реагирует на неожиданное состояние.
Удаление и переотправка сообщения выглядят как грубый инструмент. Но именно это решение оказалось надёжным, предсказуемым и понятным для пользователя. Никакой магии с таймстэмпами, никакого подбора таймаутов вслепую.
Иногда простое решение — не компромисс. Это и есть правильное решение.
Стек
Python, aiogram, asyncio. FSM для управления состояниями диалога.
// Python Dev
Другие статьи Python Dev
2026-05-15
n8n: красивая обёртка, которая съела два дня
Клиент пришёл с идеей: у них есть доступ к API level.travel, сотни Telegram-каналов для турагентов и желание автоматически публиковать выгодные туры по …
2026-05-14
Как подружить LLM с памятью: храните факты сами
LLM отлично рассуждают. С памятью у них беда. Спросите AI-ассистента о чём-то, что вы упоминали раньше в длинном диалоге — и он может запутаться, перепутать …
2026-05-13
Парсинг данных в 2026: не надо гонять каждую страницу через LLM!
Среди разработчиков парсеров распространяется странный подход: каждую скачанную страницу отправлять в LLM с просьбой найти нужные данные. Звучит удобно — не …
// Python Projects
Проекты Python Dev
2026-05-28
Робот-коллектор: автоматический обзвон должников
Система автоматического голосового обзвона должников с интеграцией с Google Таблицами, синтезом речи, распознаванием ответов и повторными попытками дозвона.
2026-05-27
Автоматическое ведение сети Telegram-каналов для турагента
Система автоматического ведения 150 Telegram-каналов с подбором туров и авиабилетов, генерацией визуалов и публикацией по расписанию.
2026-04-29
Автоматический протокол звонка: от записи до структурированного документа
Автоматический протокол звонка: от записи до структурированного документа Распределённые команды проводят много времени в звонках. Обсуждают задачи, принимают …
// Contact
Нужна помощь?
Свяжись со мной и я помогу решить проблему
Написать в TelegramОтвечаю в течение рабочего дня (03:00–13:00 GMT)
Или оставьте заявку здесь: