Telegram Inline Buttons That Spin Forever

·5 min read·Updated
telegrambotscallback-queryinline-keyboardhow-to

If your Telegram bot's inline buttons leave a spinner on the user's screen, you've hit a requirement that's easy to miss because it isn't an error: the client shows a progress bar until you answer the callback query, and you have to answer even when there's nothing to tell the user. As the API documentation puts it, "after the user presses an inline button, Telegram clients will display a progress bar until you call answer. It is, therefore, necessary to react by calling answer_callback_query even if no notification to the user is needed" (python-telegram-bot docs). The ordering that fixes it is the opposite of the instinct: answer first, then do the work.

Answer before you work, not after

The natural way to write a handler is to do the thing, then respond with the result. That's exactly backwards here — the response isn't the result, it's the acknowledgement, and every second you spend working is a second of spinner.

async def on_button(update, context):
    await update.callback_query.answer()      # first, always
    result = await do_the_work()               # then the slow part
    await update.callback_query.edit_message_text(result)

An empty answer() is a complete, valid response. It dismisses the spinner and says nothing, which is what you want for a button that quietly changes something.

When you do have something brief to say, the same call carries it — a short toast, or an alert the user must dismiss for something more important. Use the alert sparingly; it interrupts.

No deferral, and that's simpler

If you've built for Discord, you'll be looking for the equivalent of a deferred response. There isn't one, and you don't need it: answering is the acknowledgement, and editing the message afterwards is an ordinary API call rather than a followup bound to the original interaction.

That has a genuine advantage. Discord's interaction tokens expire after fifteen minutes, so long work can outlive your ability to update the message. Telegram gives you a chat ID and a message ID, and those don't expire — if you store them, you can update that message much later, from a completely different process, after any number of restarts.

For a bot doing slow work — a model call, a long fetch — that difference matters more than it first appears.

The restart problem, and why it's recoverable here

The container-shaped catch still applies: your app is replaced on every deploy and every configuration change, so work in progress is lost. A user who taps a button just before you deploy gets an answered spinner and then nothing.

The difference is that recovery is straightforward, because the address of the message is just data:

  • Store the chat ID and message ID with the pending task, on a volume, before you start the work.
  • On startup, look for unfinished tasks and either complete them or edit the message to say the operation was interrupted.
  • Keep the work short where you can, so the exposure window is small — the same advice as any other long-running handler.

Editing a message to say "that didn't finish, try again" is a much better outcome than silence, and it costs one row and one startup check.

Expect double taps

People tap buttons twice. Networks are slow, the spinner sits there, and a second tap arrives while the first is still being handled.

So make callback handling idempotent: applying it twice should produce the same state as applying it once. Toggle handlers are the usual offenders — a naive "flip the value" handler processes two taps as an on-off pair and appears to do nothing at all.

Prefer setting an explicit state over flipping one: encode "set to on" in the button rather than "toggle", so a repeat tap is harmless.

Keep callback data small and indirect

Callback data has a tight size budget, so it isn't a place to stuff a payload. Put a short key in it — an ID, an action name — and look up the details on your side.

That also keeps you honest about trust. Callback data comes back from the client, so treat it as input: check that the user pressing the button is allowed to do what it says, rather than assuming that because you sent the button, only the right person can press it. In a group chat, anyone can press anyone's buttons — which is the same reasoning as authorising the human rather than the request.

Clean up your keyboards

A small polish that avoids a class of confusion: when a button's action is no longer valid — the poll closed, the task completed, the message is stale — edit the keyboard away or replace it.

Otherwise you accumulate old messages with live-looking buttons, and every press is a handler invocation you have to reject politely. Removing the markup is one call and makes the state visible rather than something the user discovers.

FAQ

Why does my Telegram inline button show a loading spinner forever?

You haven't answered the callback query. Telegram clients show a progress bar until you do, even if you have nothing to display.

Do I have to answer even when there's no message to show?

Yes. An empty answer is valid and is exactly what a quiet button should send.

Should I answer before or after doing the work?

Before. The answer is an acknowledgement, not a result — do the work afterwards and edit the message with it.

Does Telegram have deferred responses like Discord?

No, and it doesn't need them. Answer immediately, then edit the message using its chat and message ID, which don't expire.

What happens if my app restarts mid-task?

The update never arrives. Store the chat and message ID before starting so you can finish or explain on the next startup.

How do I stop double taps causing problems?

Make handlers idempotent, and encode the target state in the button rather than a toggle, so a repeated press changes nothing.

Wrapping up

This is a one-line fix that people find after a confused evening, because nothing errors — the bot works, the action happens, and the user is left watching a spinner that means "still waiting". Answer the callback query as the first thing your handler does, keep the payload in the callback data small and untrusted, and store the message's address before you start anything slow. Telegram's model makes that last part easy: unlike an expiring token, a chat ID and a message ID are just numbers you can pick up again tomorrow.