What the 409 Conflict actually means
Telegram's Bot API documents two mutually exclusive ways of receiving updates: getUpdates, where your bot asks Telegram for them, and a webhook, where Telegram sends each update to a URL you own. Either way, Telegram keeps undelivered updates for up to 24 hours.
A long-polling bot calls getUpdates in a loop, and Telegram holds each request open until something arrives. When a second process starts polling with the same token, its request ends the first one's, and the API answers the loser with HTTP 409. The libraries turn that into their own error. python-telegram-bot raises telegram.error.Conflict, which its documentation describes as a long poll or webhook conflicting with another one. aiogram raises TelegramConflictError. grammY treats 409 as fatal: its source rethrows 401 and 409 instead of retrying them, so bot.start() stops.
The important part is that nothing is wrong with your code or with Telegram. Two copies are fighting over one token, and each one's next request knocks the other off. Messages arrive at whichever copy happens to hold the poll at that moment, which is why a conflicted bot answers some messages and ignores others.
- One token, one poller. Telegram does not share updates between two processes
- The other copy may be running different code, so users can get answers from an old version
- Restarting your bot does not fix it. The other copy is still there
Find the second copy of the bot
The other copy is almost always one you started yourself and forgot. Work through the usual places before suspecting anything stranger.
The most common case is the move to a server. You start the bot on the host, but the version on your laptop is still running in a terminal, an IDE run window or a background task. The second most common is two copies on the same machine: the bot started once by hand and once by a service manager, or started again in a new terminal while the old one still runs. Old deployments are third. A test you left running on another platform keeps polling with the same token for as long as it stays up.
If you share a bot with someone, ask them too. Anyone who has the token can run it, and BotFather's own documentation says a token can be used by anyone to control the bot.
- Your own computer: terminals, IDE run windows, scheduled tasks and anything started at login
- The server: check for two processes of the same bot, for example one started by hand and one by a supervisor
- Old hosts and test deployments that still have the token in their environment
- Teammates or collaborators who have a copy of the token
- Stop every copy but one, then watch the log: the 409s stop as soon as only one poller is left
A webhook that keeps coming back
The Bot API is explicit that getUpdates will not work while an outgoing webhook is set up, and that you cannot receive updates through getUpdates for as long as one exists. A webhook left behind by an earlier experiment, or by a framework you tried once, therefore stops a polling bot.
Check with getWebhookInfo. It returns an object whose url field is empty when the bot uses getUpdates, and filled when a webhook is set, along with pending_update_count and the most recent delivery error. deleteWebhook removes it so you can switch back to polling, and passing drop_pending_updates discards the backlog instead of replaying it. Every Bot API method answers at https://api.telegram.org/bot<token>/METHOD_NAME and accepts GET and POST. Run the request with curl from a terminal rather than pasting it into a browser, so the token does not end up in your browser history.
Many libraries already clear the webhook for you. python-telegram-bot's run_polling deletes any webhook before it starts polling, and grammY's bot.start() does the same. So if you use one of them and a webhook still reappears, something else is setting it again: an old deployment running in webhook mode re-registers its URL every time it starts. Find and stop that deployment, or the two will keep switching the bot between webhook and polling.
- Check: curl https://api.telegram.org/bot<token>/getWebhookInfo
- Remove: curl https://api.telegram.org/bot<token>/deleteWebhook
- Remove and discard the backlog: add ?drop_pending_updates=true
- Webhook keeps returning after you delete it: another deployment is calling setWebhook
When you cannot find the other copy: change the token
Sometimes the second poller is somewhere you cannot reach: a host you no longer have the login for, a machine that belonged to a former collaborator, or a token that leaked into a public repository and is being used by someone else. You cannot stop that process, but you can make its token worthless.
BotFather's documented route is the /token command, which generates a new token for a bot whose existing token is compromised or lost. Put the new token into the environment of the one copy you want to keep, restart it, and the conflict ends because nobody else has the new value.
Treat this as a security event as well as a fix. If you cannot account for a process using your token, assume someone else holds it, and replace any other secrets that sat beside it in the same file or repository.
- Send /token to @BotFather and pick the bot
- Store the new token in an environment variable, not in your code
- Update the token only where the bot should run
Stop it happening again
The habit that prevents nearly every 409 is giving development its own bot. Telegram's documentation recommends exactly this for testing: create a new bot through BotFather with /newbot and use its token in the testing instance of your code. Your laptop then polls the test bot while the server polls the real one, and the two can never collide.
Read the token from an environment variable rather than from a file in the project, so a copy of the code is not automatically a second working copy of the bot. And think about what a crash does. Because grammY rethrows a 409, a supervisor that restarts the process instantly starts a new poller that knocks the other copy off again, and the two keep taking turns. Log a 409 clearly and stop, rather than restarting in a tight loop, so the log tells you there are two copies instead of hiding it.
One rarer case: if you move a bot to your own local Bot API server, the API says you must call logOut on the cloud server first, or there is no guarantee the bot will receive updates. After that call you cannot log back in to the cloud server for 10 minutes.
- One token per running environment: a production bot and a separate test bot
- Token in the environment, never committed, so copying the code does not copy the bot
- Stop the old host before starting the new one when you move
- Log 409s at a level you will notice
Running exactly one copy around the clock
A bot that has to answer at any hour needs one copy on a machine that stays on, with something that restarts it after a crash. That is the same requirement as any 24/7 bot, covered in how to host a Discord bot 24/7, which has a section on Telegram polling and webhooks.
On Puranode, a Telegram bot runs in its own container on the Node.js or Python runtime, using long polling, which needs no public address or certificate. Set the token as an environment variable in the panel, deploy from a Git repository or upload your files, and read the live console to confirm the 409s have stopped. Stop the copy on your own computer before you start the hosted one, which is the step most moves skip. How AI agent and bot hosting works walks through the setup, and the bot hosting page lists the plans.