A Telegram bot is a program that asks the Bot API for updates, or is handed them. That is the entire architectural decision, and it decides everything else about hosting: long polling needs no domain, no certificate and no open port, while a webhook needs all three and gives you lower latency and no idle requests in return. For most bots, polling is the right answer and people reach for webhooks because they sound more professional.
This guide covers both paths properly: what BotFather actually configures, how getUpdates and setWebhook differ in practice, the one-instance rule that produces the error everyone meets eventually, the code for python-telegram-bot, grammY and Telegraf, and what a start command and deploy look like on a server. If you are also running a Discord bot, how to host a Discord bot 24/7 covers the parts that are common to any long-lived bot process.
How an update reaches your bot#
Everything a user does that involves your bot becomes an Update object on Telegram's servers. There are exactly two ways to get it, and they are mutually exclusive.
Long polling means your process calls getUpdates with a timeout, and Telegram holds the request open until something happens or the timeout expires, then you call again. The connection is outbound, so the bot works from a laptop, from behind NAT, from anywhere with outbound HTTPS. The cost is a permanent in-flight request and a small amount of added latency at the moment an update arrives.
A webhook means you call setWebhook once with a public HTTPS URL, and Telegram sends each update to that URL as an HTTP POST. There is no idle request, delivery is immediate, and the bot scales sideways if you ever need it to. The cost is that you now run a web server, need a hostname and a certificate, and have a public endpoint that anyone can guess at.
You cannot use both. If a webhook is set, getUpdates refuses with 409 Conflict: can't use getUpdates method while webhook is active. Call deleteWebhook first. The same 409 appears, with different wording, when two copies of your bot poll the same token - which is the single most common self-inflicted Telegram bot problem, and the reason a staging bot needs its own token rather than a copy of the production one.
Creating the bot: BotFather and the settings people forget#
Message @BotFather in Telegram, send /newbot, pick a display name and a username ending in bot. You get a token shaped like 123456789:AAH..., where the part before the colon is your bot's numeric ID. It is a password. /revoke issues a new one and instantly kills the old, which is the fix for a leak and also an outage until you redeploy.
Three BotFather settings change how the bot behaves and are easy to miss:
- Group privacy mode is on by default. In groups, a bot with privacy mode on only receives messages that are commands, replies to its own messages, or mentions of it. If your bot needs to read everything in a group - a moderation or logging bot - turn it off with
/setprivacy, then remove and re-add the bot to the group, because the setting is applied when the bot joins. - Commands set with
/setcommandspopulate the menu next to the input box. Users do not discover commands you never registered. You can also do this from code withsetMyCommands, which is the better option because it lives with the code that implements them. - Inline mode is off unless you enable it with
/setinline. A bot that should respond to@yourbot queryin any chat needs it switched on before the inline handler will ever fire.
Keep the token in an environment variable. Locally that is a .env file that .gitignore covers; on a server it is a variable on the Startup tab. Where to put secrets on an application server explains why a token in the repository stays dangerous long after you delete it.
Long polling in practice#
A polling bot makes one HTTP request at a time and blocks on it. The parameters that matter:
| Parameter | Sensible value | What it does |
|---|---|---|
timeout | 30 | Seconds Telegram holds the request open. 0 is short polling, avoid |
offset | last update_id + 1 | Confirms the previous batch. Without it you get them again |
limit | 100 | Updates per response, 1-100 |
allowed_updates | an explicit list | Which update types you want at all |
Every library handles offset for you, and getting it wrong by hand is why a hand-rolled bot processes the same message forever. allowed_updates is worth setting explicitly: by default chat_member updates are not sent, and if you ask for a narrow list you stop paying to receive edited messages and channel posts you will never look at.
Telegram keeps undelivered updates for up to 24 hours. That is a useful property and an unpleasant surprise: a bot that was offline all night wakes up and processes a night's worth of messages in one burst, replying to conversations that ended hours ago. If that is wrong for your bot, use drop_pending_updates when you start, which discards the backlog instead of replaying it.
The one-instance rule is absolute. Two processes polling one token fight each other, each getting a random half of the updates and both logging 409s. It happens when a deploy starts a new process before the old one has exited, when a bot is left running on a laptop, and when staging shares production's token. Handle SIGTERM, stop the poller, and let the process exit - graceful shutdown and health checks has the general pattern.
Webhooks in practice#
A webhook is a small web server plus one API call. Telegram's requirements are specific:
- HTTPS only, with a certificate from a public authority, or a self-signed one you upload with the
certificateparameter. - Port 443, 80, 88 or 8443. No other port works, which is the requirement that sinks most home-server setups.
- The URL should be unguessable. Putting the token in the path was the traditional trick; the modern answer is
secret_token, which makes Telegram send anX-Telegram-Bot-Api-Secret-Tokenheader on every request. Reject anything that does not match it, or anyone who finds your URL can feed your bot invented updates.
$ curl -X POST "https://api.telegram.org/bot$BOT_TOKEN/setWebhook" \ -d "url=https://bot.example.com/tg/hook" \ -d "secret_token=$WEBHOOK_SECRET" \ -d "max_connections=20" \ -d "allowed_updates=[\"message\",\"callback_query\"]"max_connections (1-100, default 40) caps how many deliveries Telegram will have in flight at once. On a 0.5 vCPU plan, 40 concurrent handlers is not a gift; lower it until it matches what your app can actually do in parallel.
When something is wrong, getWebhookInfo tells you exactly what:
$ curl "https://api.telegram.org/bot$BOT_TOKEN/getWebhookInfo"It returns the registered url, pending_update_count, and - the useful part - last_error_date and last_error_message, which is Telegram quoting your own server's failure back at you. A growing pending_update_count with a recent error means deliveries are failing and Telegram is retrying. A growing count with no error means your handler is too slow.
That last case is the design rule for webhooks: answer the HTTP request immediately with 200, and do the work afterwards. A handler that calls three APIs before responding holds Telegram's connection open, eats into max_connections, and eventually times out. Acknowledge, queue, process - background jobs on a small server covers the queue side when the work is genuinely slow.
On the hosting side, the requirement for one of four ports is why the proxy slot matters. Your application listens on its own port inside the container; the proxy terminates TLS on 443 in front of it, which is one of Telegram's four. Point an A record at the address shown on the proxy tab and the certificate is issued and renewed automatically, within a 21-day window before expiry. The client address arrives in X-Forwarded-For, which for a Telegram webhook is only useful if you want to restrict by Telegram's published address ranges. What a reverse proxy actually does and pointing a domain at your server cover the two halves; if the DNS side is unfamiliar, DNS records explained is the primer.
The libraries, in both modes#
The three mainstream libraries express the same two modes in about the same number of lines.
import osfrom telegram import Updatefrom telegram.ext import Application, CommandHandler, ContextTypesasync def start(update: Update, context: ContextTypes.DEFAULT_TYPE): await update.message.reply_text("Ready.")app = Application.builder().token(os.environ["BOT_TOKEN"]).build()app.add_handler(CommandHandler("start", start))# Long pollingapp.run_polling(allowed_updates=Update.ALL_TYPES)# Or a webhook, on the port the plan allocatedapp.run_webhook( listen="0.0.0.0", port=int(os.environ["PORT"]), url_path="tg/hook", webhook_url="https://bot.example.com/tg/hook", secret_token=os.environ["WEBHOOK_SECRET"],)run_webhook registers the webhook with Telegram for you and runs a small server. Note listen="0.0.0.0": binding to 127.0.0.1 means nothing outside the container can reach it, which is the commonest reason a webhook app appears to start correctly and receive nothing.
import { Bot, webhookCallback } from "grammy";import express from "express";const bot = new Bot(process.env.BOT_TOKEN);bot.command("start", (ctx) => ctx.reply("Ready."));if (process.env.MODE === "webhook") { const app = express(); app.use(express.json()); app.use("/tg/hook", webhookCallback(bot, "express", { secretToken: process.env.WEBHOOK_SECRET, })); app.listen(Number(process.env.PORT), "0.0.0.0");} else { bot.start();}Telegraf is similar: bot.launch() starts long polling and does not resolve until the bot stops, while bot.createWebhook({ domain }) returns middleware you mount on an Express app. Whichever you use, register SIGINT and SIGTERM handlers that stop the bot, because a poller killed mid-request leaves Telegram believing an instance is still attached for a few seconds - long enough for the replacement process to get a 409 on start-up.
One optimisation that only exists for webhooks: you may answer Telegram's POST with a JSON body describing a method call, and it is executed as if you had made the request. Replying to a message that way saves a whole round trip. It is not worth restructuring code for, but it is free when your handler does exactly one thing.
Putting it on a server#
The deployment shape is the same as for any resident process. Install from the lockfile or requirements.txt, run one command, restart on exit.
# Nodenpm ci --omit=dev && node bot.js# Pythonpip install --no-cache-dir -r requirements.txt && python -u bot.pypython -u, or PYTHONUNBUFFERED=1, matters more than it looks: without it your log lines sit in a buffer and the console stays empty while the bot works perfectly. How to read a server console without guessing is about telling that kind of non-problem from a real one.
Environment variables - BOT_TOKEN, WEBHOOK_SECRET, the database URL, the mode switch - go on the Startup tab. On RE:NODE the code comes from GitHub through a GitHub App with short-lived tokens, so private repositories work, and there are two switches: pull the branch on every start, and deploy on push, which restarts a server that was already running. Each deploy is one record, opened when the push lands and closed when the container is seen running again.
A webhook bot has one extra deploy consideration. The registered URL survives restarts, because it lives on Telegram's side, so you do not re-register on every boot. Do re-register when the hostname or the secret changes, and check getWebhookInfo afterwards rather than assuming. A polling bot has the opposite consideration: make sure the old process is gone before the new one starts, or the first thirty seconds of every deploy are full of 409s.
For a bot that matters, run a second one. A separate BotFather bot with its own token, its own server and the branch you are about to merge costs very little and catches the malformed handler before your users do. Staging and production on one account has the pattern, and it is the only safe way to test Telegram code, because there is no sandbox API.
Rate limits, file sizes and what Telegram will not let you do#
The Bot API has limits that are not negotiable, and designing around them late is expensive.
| Limit | Published figure |
|---|---|
| Messages to different users | About 30 per second |
| Messages to one group | About 20 per minute |
File download via getFile | 20 MB |
| File upload by the bot | 50 MB (10 MB for photos) |
Exceeding them returns 429 with a retry_after value in the response, and libraries generally wait it out. Broadcasting to a large user list therefore has to be paced by your own code: a queue, a few messages per second, and a record of who has already been sent to so a restart does not start from the beginning. Bulk sending in a tight loop is the fastest way to get your bot throttled into uselessness.
The file limits are the ones that surprise people building a media bot. They are limits of the public Bot API, not of Telegram, and they can be lifted by running the open-source local Bot API server yourself - which is a separate compiled service with its own API credentials, and belongs on a machine you control rather than alongside your app. If that is where you are heading, dedicated servers is the right shape of thing for it.
Troubleshooting#
`409 Conflict: terminated by other getUpdates request`. Two processes on one token. Find the other one: an old container, a local run, a staging copy.
`409 Conflict: can't use getUpdates method while webhook is active`. Call deleteWebhook, then poll.
`401 Unauthorized`. The token is wrong, empty or revoked. An unset environment variable produces exactly this.
The webhook is set but nothing arrives. Check getWebhookInfo for last_error_message. The usual answers are a certificate the client cannot verify, an app bound to 127.0.0.1, a path mismatch between the registered URL and the route, or a 404 from a framework that never saw the route registered.
Updates arrive but stop under load. pending_update_count climbing means your handler is slower than the arrival rate. Respond 200 first, then work.
The bot answers in private chats but ignores groups. Privacy mode. Turn it off in BotFather and re-add the bot to the group.
Everything worked, then the bot went quiet after a deploy. For pollers, a lingering old process. For webhooks, a changed hostname with a stale registration.
FAQ#
Should I use long polling or a webhook?
Long polling, unless you have a reason not to. It needs no domain, no certificate and no inbound port, it works identically in development and production, and the latency difference is a fraction of a second. Move to a webhook when you are sending a high volume of updates, when you want several instances behind one endpoint, or when an idle outbound request is genuinely a problem.
Does a Telegram bot need a domain?
Only for webhooks. A polling bot needs outbound HTTPS and nothing else. If you do go the webhook route, the proxy slot on an app plan gives you the hostname and the automatically renewed certificate, so the only DNS work is one A record.
Why do I get a 409 error every time I deploy?
Because the new process started polling before the old one stopped. Telegram allows one getUpdates caller per token. Make shutdown clean, and never run staging on the production token.
Can I host a Telegram bot for free?
You can run one at home and it will be offline whenever that machine is. Free platforms that stop an idle process do not suit a poller, which looks idle by design. There is no free tier here; the smallest app plan is 1 GB with 0.5 vCPU from $4 a month, which is more than a text bot needs.
How do I send a message to thousands of users?
Slowly, and with a record of progress. Telegram's published guidance is around 30 messages per second across different users, so queue the send, pace it, store which user IDs have been handled, and expect a 429 with retry_after if you push. A broadcast that restarts from zero after a crash is worse than a slow one.
Can one server run several bots?
Two small bots fit in one container, but they share a memory limit and a CPU share, so one crash loop takes the other down with it. Separate plans keep the blast radius small, and the console graphs stay readable because each one shows a single process.




Комментарии
Полностью анонимно: без аккаунта, без почты, без cookie. Мы храним имя, которое вы ввели, текст и время - больше ничего. Количество ссылок ограничено, разметка не отображается.