· 6 min read

Telegram + AI: Stop Letting Your Bot Block on Model Calls

A common Telegram + AI mistake is treating the bot like a normal request/response web app and waiting for the model before acknowledging the update. In Telegram, that pattern creates slow replies, retry storms, duplicate messages, and a bad user experience.

A very common mistake in Telegram AI bots is simple: the bot handler waits for the model response before it sends anything back to Telegram.

That feels natural if you are building a normal HTTP app. It is a bad fit for Telegram updates.

Telegram expects your bot to handle incoming updates quickly and reliably. If you make the update handler sit idle while an LLM call runs, you can get:

  • slow perceived response times
  • duplicate processing when updates are retried
  • webhook timeouts
  • poor concurrency under load
  • a bot that feels frozen whenever the model is slow

This guide explains why the mistake happens, what breaks, and how to structure a Telegram + AI bot so it stays responsive.

The mistake: synchronous model calls inside the update handler

A typical broken implementation looks like this:

async def on_message(update, context):
    user_text = update.message.text
    reply = await call_llm(user_text)
    await update.message.reply_text(reply)

Or, in a webhook server:

@app.post("/telegram/webhook")
async def telegram_webhook(update: dict):
    text = update["message"]["text"]
    answer = llm_client.generate(text)
    return {"method": "sendMessage", "text": answer}

Both versions have the same flaw: they tie Telegram update handling to model latency.

That is dangerous because AI calls are not deterministic. Even a healthy model request may take 1–10 seconds, and sometimes much longer. Telegram infrastructure and your users do not care that your prompt is complex.

Why this breaks in production

1. Webhook timeout pressure

If you use webhooks, your bot must respond quickly enough for Telegram and your reverse proxy. If your handler spends too long waiting on the model, the request can time out. Once that happens, Telegram may retry the update.

Retries are not harmless. If your bot already started processing the update, the retry can create duplicate replies unless you built idempotency into the system.

2. Message ordering becomes fragile

If one update blocks on an LLM call, later updates from the same chat may queue behind it. That is a bad experience for interactive bots where users expect fast acknowledgments.

3. Slow model calls look like bot downtime

Users do not distinguish between “the model is slow” and “the bot is broken.” If your bot stays silent for 8 seconds, people assume failure.

4. One slow call can tie up a worker

In a polling or single-worker setup, a slow LLM request can reduce throughput for all users. A burst of messages can quickly overwhelm the process.

The better pattern: acknowledge fast, process asynchronously

Your bot should separate three concerns:

  1. Receive the Telegram update
  2. Acknowledge it immediately
  3. Process the AI task in the background

For AI-driven bots, the rule is simple:

Never make Telegram wait for the model.

Instead, send a quick acknowledgment such as:

  • “Thinking…”
  • “Working on it.”
  • a typing action
  • a queued job reference

Then do the model call in a worker, task queue, or background coroutine.

A production-safe design usually looks like this:

  • Webhook receiver: accepts the Telegram update quickly
  • Deduplication layer: stores update IDs or message IDs to prevent duplicate processing
  • Job queue or background task: handles AI generation separately
  • AI worker: calls the model, applies prompts, filters output
  • Telegram sender: posts the final reply back to the chat

Minimal flow

  1. User sends a message.
  2. Telegram delivers the update.
  3. Your bot stores the update ID and immediately returns 200 OK.
  4. Your bot sends a short placeholder reply or typing indicator.
  5. A worker calls the model.
  6. The worker sends the final answer.

Good implementation patterns

Pattern 1: background task in a small bot

If your bot has modest traffic, an in-process background task may be enough.

async def on_message(update, context):
    chat_id = update.message.chat_id
    text = update.message.text

    await update.message.reply_text("Thinking...")
    context.application.create_task(process_ai_reply(chat_id, text, context.bot))

async def process_ai_reply(chat_id, text, bot):
    answer = await call_llm(text)
    await bot.send_message(chat_id=chat_id, text=answer)

This is better than blocking the handler, but it still has limitations:

  • tasks disappear if the process restarts
  • duplicate protection is weak unless you add state
  • large bursts can still overload the process

Use this only for simple bots or prototypes.

Pattern 2: queue-based worker for production

For real traffic, enqueue the job and let a worker handle it.

async def on_message(update, context):
    update_id = update.update_id
    chat_id = update.message.chat_id
    text = update.message.text

    if already_processed(update_id):
        return

    mark_processed(update_id)
    enqueue_job({
        "chat_id": chat_id,
        "text": text,
        "message_id": update.message.message_id,
    })

    await update.message.reply_text("Thinking...")

Worker:

async def handle_job(job):
    answer = await call_llm(job["text"])
    await bot.send_message(chat_id=job["chat_id"], text=answer)

This gives you:

  • retry safety
  • better scaling
  • rate limiting control
  • easier observability

Handle duplicates explicitly

Telegram can retry updates. Your AI pipeline must assume at-least-once delivery.

A common fix is to deduplicate using:

  • update_id
  • message_id
  • a chat-scoped request key

Store the key before starting the model call.

Example logic:

if redis.setnx(f"telegram:update:{update_id}", "1"):
    redis.expire(f"telegram:update:{update_id}", 86400)
    enqueue_job(...)

If the key already exists, skip processing.

Do not wait until after the model call to mark the update as seen. By then it may already be retried.

Use typing and progress signals wisely

Sending typing... is useful, but it is not a substitute for proper async handling.

Use it as a user experience improvement:

  • send sendChatAction while the job runs
  • post a short acknowledgment immediately
  • if generation takes long, consider a progress update or partial result

Do not spam status messages. One short acknowledgment is enough for most cases.

Watch out for the “reply-edit” pattern

Some developers send one placeholder message and then edit it with the AI output.

That can work well for chat UX:

  1. Send Processing...
  2. Later edit the message with the final answer

But keep two cautions in mind:

  • editing can fail if the message is too old or changed
  • if the model returns slowly, the placeholder can look stale

This is still better than blocking the handler, as long as the edit happens from an async worker.

Apply timeouts and cancellation

LLM calls should always have a timeout.

Without one, a stuck request can tie up your worker indefinitely.

Example policy:

  • 15 seconds for normal chat replies
  • 30 seconds for more complex generation
  • fallback message if the model times out
try:
    answer = await asyncio.wait_for(call_llm(text), timeout=15)
except asyncio.TimeoutError:
    answer = "Sorry, that took too long. Please try again."

If your bot supports conversation state, make sure canceled or timed-out jobs do not continue and later send stale replies.

Keep AI context separate from transport logic

Another subtle mistake is mixing prompt construction, Telegram parsing, and message delivery in one handler.

That makes debugging painful.

Instead, separate layers:

  • Telegram adapter: extracts text, chat ID, user metadata
  • Conversation service: builds prompt and conversation state
  • Model client: talks to the AI provider
  • Delivery layer: sends or edits Telegram messages

This separation makes it easier to test the AI pipeline without Telegram and to test Telegram delivery without model dependencies.

Test failure modes, not just happy paths

Most bot bugs show up under stress or retries, not in normal chats.

Test these cases explicitly:

  • model latency of 1s, 5s, 20s
  • duplicate webhook delivery
  • Telegram API rate limits
  • worker restart mid-job
  • network failure while sending the final reply
  • user sends two messages before the first AI reply returns

If your bot behaves well only when the model is fast, it is not production-ready.

A simple rule of thumb

If your Telegram handler is waiting on the model, the architecture is wrong.

The handler should do the minimum necessary work to safely hand off the job. Everything else belongs in background processing.

Final checklist

Before shipping a Telegram + AI bot, verify that you have:

  • fast webhook acknowledgment
  • background or queued model processing
  • deduplication by update ID or message ID
  • explicit timeouts on model calls
  • a user-visible acknowledgment message or typing indicator
  • separate Telegram, prompt, and delivery layers
  • retry-safe behavior for webhook re-delivery

If you fix only one thing, fix the blocking handler. It is the most common reason Telegram AI bots feel unreliable even when the model itself works fine.

Build for asynchronous delivery first, and your bot will be much easier to scale, debug, and trust.

← All posts