A comprehensive, beginner-friendly guide and production-ready starter template for migrating your existing Python Telegram bot (polling or webhook) to TypeScript running serverless on Cloudflare Workers.
Say goodbye to paying $5–$20/month for a VPS or managing crashing server processes — get 0ms cold start latency, automatic global scaling, and 100,000 requests/day for free.
| Feature | Traditional Python Bot (VPS / Heroku / EC2) | Cloudflare Workers + TypeScript |
|---|---|---|
| Hosting Cost | $5 – $25/month for VPS | $0.00 / Free Tier (100,000 requests/day) |
| Delivery Model | Long Polling (run_polling()) |
Ultra-fast Serverless Webhook |
| Server Maintenance | Linux patches, memory leaks, PM2, Docker | Zero Maintenance (Fully managed by Cloudflare) |
| Cold Starts | 1–5 seconds process boot | 0ms cold start (V8 isolates) |
| Global Latency | Single datacenter location | Deployed across 300+ edge cities worldwide |
| Crash Recovery | Bot dies if uncaught exception occurs | Each request is an isolated worker event |
Most Python bots built using libraries like python-telegram-bot or telebot use Long Polling. This requires a process running 24/7 on a server constantly asking Telegram: "Do you have new messages?".
Cloudflare Workers use Webhooks, which are push notifications sent by Telegram whenever an event occurs:
flowchart TD
subgraph Polling ["❌ Traditional Python Long Polling (Requires 24/7 Paid Server)"]
direction TB
A1[Python Bot Server] -->|Continuously loops & polls every few seconds| B1[Telegram Server]
B1 -->|Returns updates| A1
end
subgraph Webhook ["✅ Cloudflare Workers Serverless Webhook (Event-Driven & Free)"]
direction TB
U[User] -->|Sends Telegram message| TG[Telegram Server]
TG -->|Instant HTTP POST Webhook| CW[Cloudflare Edge Worker]
CW -->|Processes update in milliseconds| TG
TG -->|Delivers reply to user| U
end
This repository uses a clean, beginner-friendly single-file architecture with zero unnecessary complexity:
telegram-bot-cloudflare-worker/
├── index.ts # All-in-one TypeScript bot (types, helpers, commands & webhook)
├── .dev.vars.example # Template for local environment secrets
├── .gitignore # Files excluded from git tracking
├── package.json # Project scripts, types & dependencies
├── tsconfig.json # TypeScript compiler settings
├── wrangler.jsonc # Cloudflare Wrangler configuration
├── README.md # Complete tutorial & handbook
└── LICENSE # MIT License
Before you start, make sure you have:
- Node.js (v18+) installed. Verify in your terminal:
node -v npm -v
- A free Cloudflare Account: Sign up here.
- A Telegram Bot Token:
- Open Telegram and search for @BotFather.
- Send
/newbotand follow the prompts to get your bot token (e.g.,123456789:ABCdefGHIjkl...).
git clone https://github.com/SahasGamageX/telegram-bot-cloudflare-worker.git
cd telegram-bot-cloudflare-worker
npm installIf you prefer starting completely from scratch:
npm create cloudflare@latest my-bot -- --type=hello-world --ts
cd my-bot
npm install -D @cloudflare/workers-typesIn Python, your bot might look like this:
# Python: bot.py
async def start(update, context):
await update.message.reply_text("Hello from Python!")In TypeScript on Cloudflare Workers, we handle incoming requests inside the fetch handler:
// src/index.ts
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Only accept POST requests from Telegram
if (request.method !== "POST") {
return new Response("Bot is active and running.", { status: 200 });
}
const update = await request.json();
if (update.message?.text === "/start") {
await sendMessage(env, update.message.chat.id, "Hello from TypeScript!");
}
return new Response("OK", { status: 200 });
}
};Never hardcode your bot token in code!
- Copy the example environment file:
cp .dev.vars.example .dev.vars
- Fill in your credentials inside
.dev.vars:BOT_TOKEN="123456789:ABCdefGHIjklMNOpqrsTUVwxyz" WEBHOOK_SECRET="my_random_secret_token_123"
- Start the local worker emulator:
npm run dev
- How to test locally:
Because Telegram cannot send webhooks directly to
http://localhost:8787, you can test your worker logic locally usingcurl:If configured properly, your Telegram account will receive thecurl -X POST http://127.0.0.1:8787 \ -H "Content-Type: application/json" \ -H "X-Telegram-Bot-Api-Secret-Token: my_random_secret_token_123" \ -d '{"update_id":1,"message":{"chat":{"id":123456789},"text":"/start"}}'
/startmessage!
-
Log in to Wrangler from your terminal:
npx wrangler login
(A browser window will open — click Authorize).
-
Upload your bot secrets to Cloudflare's encrypted key-value store:
npx wrangler secret put BOT_TOKEN npx wrangler secret put WEBHOOK_SECRET
(Paste your respective values when prompted).
-
Deploy your code:
npm run deploy
Wrangler will output your live public Worker URL:
https://telegram-bot-cloudflare-worker.<your-subdomain>.workers.dev
Now notify Telegram to deliver incoming messages to your Cloudflare Worker URL.
Run this curl command (replace placeholders with your real values):
curl -F "url=https://telegram-bot-cloudflare-worker.<your-subdomain>.workers.dev" \
-F "secret_token=YOUR_WEBHOOK_SECRET" \
https://api.telegram.org/botYOUR_BOT_TOKEN/setWebhookYou should receive a JSON response confirming:
{"ok":true,"result":true,"description":"Webhook was set"}🔒 Why use
secret_token? Anyone who discovers your Cloudflare Worker URL could theoretically send fake requests to it. By providing asecret_token, Telegram will send anX-Telegram-Bot-Api-Secret-Tokenheader on every call. Your Worker verifies this header, instantly dropping unauthorized spam!
Verify that Telegram has zero pending updates and no errors:
curl https://api.telegram.org/botYOUR_BOT_TOKEN/getWebhookInfoTo see incoming requests and debug in real-time while chatting with your bot:
npm run tail(Runs wrangler tail to stream logs directly to your console).
from telegram import Update
from telegram.ext import ContextTypes
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("👋 Hello from Python!")import { Env } from "./types";
import { sendMessage } from "./telegram";
export async function handleStart(chatId: number, env: Env) {
await sendMessage(env, chatId, "👋 Hello from TypeScript on Cloudflare Workers!");
}from telegram import InlineKeyboardButton, InlineKeyboardMarkup
keyboard = [
[InlineKeyboardButton("🌐 Website", url="https://example.com")],
[InlineKeyboardButton("⚡ Click Me", callback_data="btn_clicked")]
]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text("Please choose:", reply_markup=reply_markup)import { sendMessage } from "./telegram";
await sendMessage(env, chatId, "Please choose an option:", {
reply_markup: {
inline_keyboard: [
[{ text: "🌐 Website", url: "https://example.com" }],
[{ text: "⚡ Click Me", callback_data: "btn_clicked" }]
]
}
});When a user clicks an inline button, Telegram displays a loading spinner on the button until you call answerCallbackQuery.
async def button_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
await query.answer(text="Button was clicked!", show_alert=False)import { answerCallbackQuery } from "./telegram";
export async function handleCallback(callbackQuery: TelegramCallbackQuery, env: Env) {
await answerCallbackQuery(env, callbackQuery.id, {
text: "Button was clicked!",
show_alert: false
});
}export async function sendPhoto(env: Env, chatId: number, photoUrl: string, caption?: string) {
await fetch(`https://api.telegram.org/bot${env.BOT_TOKEN}/sendPhoto`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat_id: chatId,
photo: photoUrl,
caption: caption,
parse_mode: "HTML"
})
});
}In traditional Python scripts, you might have used SQLite (bot.db) or kept state in memory (user_steps = {}).
Because Workers run across a decentralized network, local disk storage is not available. Here are the best free alternatives:
Ideal for user sessions, simple state tracking, and rate limiting:
npx wrangler kv:namespace create BOT_KVUsage in TypeScript:
// Save user step
await env.BOT_KV.put(`user:${chatId}:step`, "awaiting_name");
// Read user step
const step = await env.BOT_KV.get(`user:${chatId}:step`);If your Python bot used SQLite tables, Cloudflare D1 is a drop-in relational SQL database with a generous free tier:
npx wrangler d1 create bot-databaseUsage in TypeScript:
const { results } = await env.DB.prepare(
"SELECT * FROM users WHERE telegram_id = ?"
).bind(chatId).all();- Trap: In Python, you could create a global dictionary:
sessions = {}. - Workers Reality: Cloudflare Workers are ephemeral. Instances spin up and down constantly across 300+ cities. Global RAM variables will get wiped. Always persist data in Cloudflare KV or D1.
- Trap: If your bot does slow tasks (e.g., calling OpenAI, scraping a website, or generating images), Telegram expects a
200 OKresponse within 5 seconds. If delayed, Telegram assumes the webhook failed and retries sending the same message repeatedly, causing duplicates. - Solution: Return
200 OKimmediately and let background work continue usingctx.waitUntil():export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> { const update = await request.json(); // Run slow background tasks without blocking response ctx.waitUntil(processSlowTask(update, env)); return new Response("OK", { status: 200 }); } };
- Cloudflare Free Tier gives you up to 10ms–50ms CPU execution time.
- Waiting for external network requests (like calling
fetch(https://api.telegram.org/...)or an external API) is classified as I/O wait time and does NOT count against your CPU time limit.
If you ever want to run your bot locally via Python polling again, remember to delete the active webhook:
curl https://api.telegram.org/botYOUR_BOT_TOKEN/deleteWebhookFor large bots with hundreds of commands, deep conversation trees, and complex plugins, you can use grammY — the modern TypeScript Telegram Bot framework with official first-class support for Cloudflare Workers.
Contributions, suggestions, and improvements are welcome!
- Found a bug? Open an Issue.
- Want to add a new example? Submit a Pull Request.
- If this guide helped you, please give it a ⭐ Star on GitHub to help others find it!
This tutorial and template are open source and licensed under the MIT License. Feel free to use it for personal or commercial bots!