EthioDeploy
DocsPricingDomainsBlogChangelog
Deploy guides/Telegram bot

Host a Telegram bot

Run a Telegram bot as a web service that receives webhooks, or as a Background Worker that long-polls. Both work on the Free plan. A worker never sleeps; a webhook bot sleeps and wakes on the next message.

Last updated October 29, 2026

What we run by default
Detected by
package.json, requirements.txt or pyproject.toml
Install
npm ci / pnpm / yarn, or pip install -r requirements.txt
Build
npm run build (if you have one)
Start
npm start, or python main.py

Override any of them in Settings → Build.

Fix this first: A polling bot must be a Background Worker

A bot that calls getUpdates (python-telegram-bot's run_polling(), grammY's bot.start()) serves no HTTP, so a web service's health check never passes and the deploy fails. Set Service Type to Background Worker in Settings → Build, or switch the bot to webhooks.

Webhook or long polling

Telegram delivers updates in one of two ways, and a bot can only use one at a time:

  • Webhook. Telegram sends each update as an HTTPS POST to your app. Deploy it as a normal web service. On the Free plan it sleeps after 30 minutes without traffic and wakes on the next update, so the first reply after a quiet spell waits for the app to boot.
  • Long polling. Your bot asks Telegram for updates in a loop. Deploy it as a Background Worker. A worker never sleeps on the Free plan, but its memory and CPU count against the monthly usage quota.

For a small bot on the Free plan, long polling as a worker is the simpler choice: no public URL, no health route, and no wait after a quiet spell. Use a webhook when the bot is part of a web app, or when you'd rather sleep than spend quota.

Webhook mode as a web service

The app needs two routes: a POST route Telegram sends updates to, and a GET / that returns 200 for the health check. Listen on 0.0.0.0 and the PORT env var. Check the X-Telegram-Bot-Api-Secret-Token header, and answer 200 fast: Telegram re-sends an update when the request doesn't finish in time.

Python, following python-telegram-bot's custom webhook example (add python-telegram-bot, starlette and uvicorn to requirements.txt):

# main.py
import asyncio
import os

import uvicorn
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import PlainTextResponse, Response
from starlette.routing import Route
from telegram import Update
from telegram.ext import Application, CommandHandler

SECRET = os.environ["WEBHOOK_SECRET"]


async def start(update, context):
    await update.message.reply_text("Hi!")


async def main():
    bot = Application.builder().token(os.environ["BOT_TOKEN"]).updater(None).build()
    bot.add_handler(CommandHandler("start", start))

    async def telegram(request: Request) -> Response:
        if request.headers.get("X-Telegram-Bot-Api-Secret-Token") != SECRET:
            return Response(status_code=403)
        await bot.update_queue.put(Update.de_json(data=await request.json(), bot=bot.bot))
        return Response()

    async def health(request: Request) -> PlainTextResponse:
        return PlainTextResponse("ok")

    web = Starlette(routes=[
        Route("/telegram", telegram, methods=["POST"]),
        Route("/", health, methods=["GET"]),
    ])
    port = int(os.environ.get("PORT", 3000))
    server = uvicorn.Server(uvicorn.Config(web, host="0.0.0.0", port=port))

    async with bot:
        await bot.start()
        await server.serve()
        await bot.stop()


asyncio.run(main())

The update goes on a queue and the route returns at once, so a slow handler never holds up Telegram. With no start command set, we run python main.py, which is what this needs.

Node, with grammY and Express (npm install grammy express):

// index.js
const express = require("express");
const { Bot, webhookCallback } = require("grammy");

const bot = new Bot(process.env.BOT_TOKEN);
bot.command("start", (ctx) => ctx.reply("Hi!"));

const app = express();
app.use(express.json());
app.get("/", (req, res) => res.send("ok"));
app.post(
  "/telegram",
  webhookCallback(bot, "express", { secretToken: process.env.WEBHOOK_SECRET })
);

app.listen(process.env.PORT || 3000);

grammY compares the secret header for you. Don't call bot.start() in webhook mode; that starts long polling. Add "start": "node index.js" to your package.json scripts.

Point Telegram at your app

After the first deploy, register the webhook once. Pick a secret of 1 to 256 letters, digits, _ or -, save it as WEBHOOK_SECRET in the project's settings, and run:

curl "https://api.telegram.org/bot<token>/setWebhook?url=https://<project-slug>.ethiodeploy.app/telegram&secret_token=<secret>"

The URL uses port 443, which Telegram accepts. Your app still listens on PORT; HTTPS ends at the platform's proxy. To see whether deliveries work, call getWebhookInfo:

curl "https://api.telegram.org/bot<token>/getWebhookInfo"

A growing pending_update_count or a last_error_message means Telegram can't reach the route or isn't getting a 2xx back.

What happens when a sleeping bot gets a message

On the Free plan, a web service stops after 30 minutes without traffic. When the next update arrives, we start the app and hold Telegram's request for up to 30 seconds while it boots. Once your health check passes, the request goes through to your app unchanged, and the bot replies. The user just waits a few seconds longer.

If the app takes longer than 30 seconds to boot, Telegram gets a 503. Telegram repeats a delivery that doesn't get a 2xx and gives up after “a reasonable amount of attempts”. Each retry finds the app booting or awake, so it normally lands. Keep startup light: don't load large files or call slow APIs before the server starts listening.

Redeploys don't cause this wait. A web service deploys side by side, and the old version answers until the new one is healthy.

Long polling as a Background Worker

Set Service Type to Background Worker when you create the project, or later in Settings → Build. A worker gets no domain and no HTTP health check, so you don't need a health route or PORT. The deploy passes once the process is still running after a short startup grace period.

# main.py (python-telegram-bot)
import os
from telegram.ext import Application, CommandHandler


async def start(update, context):
    await update.message.reply_text("Hi!")


app = Application.builder().token(os.environ["BOT_TOKEN"]).build()
app.add_handler(CommandHandler("start", start))
app.run_polling()
// index.js (grammY)
const { Bot } = require("grammy");

const bot = new Bot(process.env.BOT_TOKEN);
bot.command("start", (ctx) => ctx.reply("Hi!"));
bot.start();

Only one process can poll a bot token at a time. A worker deploy stops the old instance before it starts the new one, so two copies never poll at once. That costs a few seconds of downtime per deploy. Telegram keeps updates for up to 24 hours until the bot fetches them, so messages sent in that gap still arrive.

Polling doesn't work while a webhook is set. If the bot used webhooks before, call deleteWebhook first:

curl "https://api.telegram.org/bot<token>/deleteWebhook"

Health check

This applies to webhook mode only. Every deploy requests your health check path (/ by default) and only goes live once it returns a 2xx or 3xx status. The platform keeps checking it after that. If / returns 404 or needs a login, add a route like /healthz that returns 200 and set it as the Health Check Path in Settings. Otherwise the deploy fails and the previous version keeps serving. Telegram only POSTs to /telegram, so without a GET / route the check gets a 404 or 405.

Environment variables

Add variables in the project's settings. They're available both while the app builds and while it runs, so build-time values (like framework-public keys) work without extra setup. See Environment variables. Put the token from BotFather in BOT_TOKEN and, for webhook mode, your secret in WEBHOOK_SECRET. Keep both out of the repo.

Host a Telegram bot for free

The Free plan costs nothing and needs no card. It includes:

  • One project, live over HTTPS at <project-slug>.ethiodeploy.app.
  • 256 MB of memory and half a CPU core.
  • A managed Postgres, MySQL or Redis addon with 512 MB of storage. It doesn't expire.
  • A redeploy on every push, the same as the paid plans.

The catch is sleep. After 30 minutes without traffic the app stops, and the next request starts it again. We hold that request for up to 30 seconds while the app boots, so the first visitor after a quiet spell waits a little.

Sleep only applies to web services. A Background Worker keeps polling around the clock, but it counts against the Free plan's monthly usage quota: 50 GB-hours of memory and 20 CPU-hours, measured from what the bot actually uses. A bot averaging about 65 MB of memory uses close to 50 GB-hours in a 31-day month. If you go over and stay over for 14 days, the project stops until the next month or until you upgrade.

A webhook bot only uses quota while it's awake, but the first reply after a quiet spell waits for the boot. The Free plan has one project, so if you also run a website, add the webhook route to that app instead of deploying the bot separately.

When the app has to stay awake or needs your own domain, Pro is $10 or 1,399 ETB a month: always on, 1 GB of memory and a 5 GB database.

Deploy

  1. Sign in with GitHub and install the EthioDeploy app on the repo.
  2. Click “New Project” and pick the repo.
  3. Watch the build in the dashboard. When the health check passes, your app is live at <project-slug>.ethiodeploy.app. Every push to the default branch redeploys, and the old version keeps serving until the new one is healthy.

Troubleshooting

409 Conflict errors in the logs, or the bot stops answering+

Cause: Two processes are polling the same bot token, such as a copy on your laptop or another host, or a polling bot deployed as a web service, where deploys briefly run old and new side by side.

Fix: Stop the other copy, and deploy polling bots as a Background Worker so deploys never overlap.

The webhook is set but the bot never replies+

Cause: Telegram can't reach the route, gets a non-2xx answer, or your app rejects the secret header.

Fix: Call getWebhookInfo and read last_error_message. Check the URL path matches your POST route, and that WEBHOOK_SECRET matches the secret_token you passed to setWebhook.

Deploy fails: the app never answered on port $PORT+

Cause: The server listens on 127.0.0.1 or a fixed port. python-telegram-bot's run_webhook() defaults to 127.0.0.1 and port 80. Port 8443 is for Telegram's side of the webhook, not yours.

Fix: Listen on 0.0.0.0 and PORT. Telegram reaches you on 443 through the platform's proxy.

Deploy fails: GET / returned 404 or 405+

Cause: The app only has the POST webhook route, so the health check never passes.

Fix: Add a GET / route that returns 200, or set another GET route as the Health Check Path.

A polling bot fails to start after switching from webhooks+

Cause: getUpdates doesn't work while a webhook is set.

Fix: Call deleteWebhook once, then redeploy.

Still stuck? Check the deployment's build log in the dashboard, or see Troubleshooting.

Questions

Yes, within limits. As a Background Worker, a long-polling bot runs without sleeping, but its memory and CPU count against the Free plan's monthly quota of 50 GB-hours and 20 CPU-hours. A small bot fits; one that stays over the quota for 14 days is stopped until the next month. As a webhook web service it answers at any hour but sleeps after 30 minutes without traffic, so the first reply after a quiet spell waits for a boot. Pro ($10/month) keeps a web service always on.

On the Free plan, long polling as a Background Worker is simpler and never sleeps. Use a webhook if the bot lives inside a web app, or if you'd rather it sleep when idle than use quota.

Usually not. Telegram keeps updates for up to 24 hours until the bot receives them, and it repeats a webhook delivery that doesn't get a 2xx response. A message can be lost if the bot stays unreachable longer than Telegram keeps retrying.

The PORT env var, on 0.0.0.0. Telegram's port list (443, 80, 88, 8443) is about the public webhook URL, which is https://<project-slug>.ethiodeploy.app on 443.

Ready to deploy?

Sign in with GitHub and pick your repo. No Dockerfile, no YAML.

More guides

Firebase StudioReplitv0

All guides on the deploy guides page. Custom domains, env vars, addons and billing are in the docs.

On this page
Webhook or long pollingWebhook mode as a web servicePoint Telegram at your appWhat happens when a sleeping bot gets a messageLong polling as a Background WorkerHealth checkEnvironment variablesHost a Telegram bot for freeDeployTroubleshootingQuestions
Deploying with an agent?Point it at llms.txt. Every guide is in one file there.Open llms-full.txt →
EthioDeploy

Agentic first development and deployment, started in Ethiopia, built for developers everywhere.

Telegram community
Product
PricingAI coding agentAgent mode docsDomainsCLIFree backend hostingHosting in EthiopiaChangelog
Deploy guides
Next.jsNode.jsPythonSvelteKitTanStack StartDjangoFastAPIFlaskExpressNestJSLaravelRailsGoTelegram botLovableBoltReplitv0Google AI StudioBase44Firebase StudioClaude Code & Cursor
Compare
vs Herokuvs Vercelvs Rendervs Railwayvs Fly.iovs NetlifyHeroku alternativesVercel alternativesNetlify alternativesRender alternativesRailway alternatives
Resources
DocsBlogAboutSupportContactllms.txt
© 2026 EthioDeployPay by card in USD or bank transfer in ETBTermsPrivacy