Implementing Telegram Bot in Python and Javascript with deployment to AWS
And deploying new Telegram bot to AWS
Here are my notes with step-by-step tutorial on how to implement and deploy to AWS a Telegram bot. I’ve added a quick start (long polling) and a production-ready path (webhooks), with examples in Python and Node.js.
Telegram bots are apps that run on your servers and talk to users through Telegram. We’ll register a bot, write a tiny program, and connect it to Telegram’s Bot API.
TL;DR
- Create a bot via @BotFather → get token.
- Build a small app with python-telegram-bot or Telegraf.
- Start with long polling locally; switch to webhooks for production.
What you’ll need
-
A Telegram account (mobile or desktop)
-
Basic terminal + code editor
-
One of:
- Python 3.10+ and
pip
- Node.js 18+ and
npm
/pnpm
/yarn
- Python 3.10+ and
Create your bot with BotFather (get the token)
- In Telegram, search for @BotFather and start a chat.
- Send
/newbot
and follow the prompts to name your bot and choose a unique username (must end withbot
), e.g.example_helper_bot
. - BotFather replies with an API token like
123456:ABC-DEF...
. Treat this like a password; don’t commit it to Git. You can always regenerate it via BotFather if it leaks.
Tip: Telegram’s official tutorial walks through these exact steps and how to verify your token with
getMe
.
Pick your stack
You can call the raw HTTP Bot API yourself, but using a maintained library is faster.
- Python:
python-telegram-bot
(async, modern). ([https://docs.python-telegram-bot.org]) - Node.js:
telegraf
(mature, TypeScript-friendly). ([https://telegraf.js.org])
The Bot API itself is documented here and is the source of truth for methods like getUpdates
and setWebhook
.
Quick Start: run locally with long polling
Long polling is perfect for local development: your bot repeatedly asks Telegram for new updates via getUpdates
. (In production, you’ll likely switch to webhooks.)
Telegram supports two mutually exclusive ways to receive updates:
getUpdates
(polling) or webhooks; updates are kept for up to 24 hours.
Option A: Python (with python-telegram-bot
)
Install & scaffold
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install python-telegram-bot
Code: bot.py
import os
from telegram import Update
from telegram.ext import ApplicationBuilder, CommandHandler, MessageHandler, ContextTypes, filters
TOKEN = os.environ["TELEGRAM_BOT_TOKEN"] # export TELEGRAM_BOT_TOKEN=123:ABC
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("👋 Hello! Send me something and I’ll echo it.")
async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(update.message.text)
def main():
app = ApplicationBuilder().token(TOKEN).build()
app.add_handler(CommandHandler("start", start))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
app.run_polling() # long polling
if __name__ == "__main__":
main()
Run it:
export TELEGRAM_BOT_TOKEN=123456:ABC-DEF...
python bot.py
This uses ApplicationBuilder
from the current stable docs.
Option B: Node.js (with telegraf
)
Install & scaffold
npm init -y
npm i telegraf
Code: index.js
const { Telegraf } = require('telegraf');
const bot = new Telegraf(process.env.BOT_TOKEN); // set BOT_TOKEN env var
bot.start((ctx) => ctx.reply('👋 Hello! Send me something and I’ll echo it.'));
bot.on('text', (ctx) => ctx.reply(ctx.message.text));
bot.launch();
// graceful stop
process.once('SIGINT', () => bot.stop('SIGINT'));
process.once('SIGTERM', () => bot.stop('SIGTERM'));
Run it:
export BOT_TOKEN=123456:ABC-DEF...
node index.js
This mirrors the official Telegraf “Getting started” example.
Make it useful (commands & buttons)
Add a /help
command
Python
from telegram.ext import CommandHandler
async def help_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("/start – greet\n/help – this help")
app.add_handler(CommandHandler("help", help_cmd))
CommandHandler
is the standard way to bind /commands
.
Node (Telegraf)
bot.help((ctx) => ctx.reply('/start – greet\n/help – this help'));
Quick reply buttons (custom keyboard)
Python
from telegram import ReplyKeyboardMarkup
async def start(update: Update, context):
keyboard = [["Help", "About"]]
await update.message.reply_text(
"Choose an option:",
reply_markup=ReplyKeyboardMarkup(keyboard, resize_keyboard=True)
)
Node (Telegraf)
const { Markup } = require('telegraf');
bot.start((ctx) => {
ctx.reply("Choose an option:", Markup.keyboard([["Help", "About"]]).resize());
});
Production: switch to webhooks
In production, Telegram pushes updates to your HTTPS endpoint. You can set this with setWebhook
, optionally providing a secret token so you can verify the request via the X-Telegram-Bot-Api-Secret-Token
header.
Set a webhook with curl
:
TOKEN=123456:ABC-DEF...
WEBHOOK_URL="https://your-domain.com/telegram/${TOKEN}" # include a secret path
SECRET="my-secret-42"
curl -X POST "https://api.telegram.org/bot${TOKEN}/setWebhook" \
-d url="${WEBHOOK_URL}" \
-d secret_token="${SECRET}" \
-d drop_pending_updates=true
Key points from the spec:
setWebhook
requires an HTTPS URL and supportssecret_token
.- While a webhook is set, you cannot use
getUpdates
. - Supported ports include 443, 80, 88, 8443.
Webhook server options
- Node (Telegraf) built-in webhook:
bot.launch({
webhook: {
domain: "your-domain.com",
port: 8080,
// optional secret header
secretToken: process.env.WEBHOOK_SECRET
}
});
Telegraf exposes first-class webhook options and integrates with Express/Fastify/Cloudflare Workers, etc.
- Python (
python-telegram-bot
) PTB works well with ASGI/WSGI frameworks (FastAPI, Starlette, Flask). UseApplicationBuilder().token(...).build()
, expose a POST route that feeds incoming JSON updates into your application, and callbot.set_webhook(...)
. See theApplicationBuilder
docs for construction patterns. ([docs.python-telegram-bot.org][2])
Testing locally? Use a tunnel (e.g.,
ngrok
) to exposehttps://...
to Telegram, then callsetWebhook
with the public URL.
Configure bot commands (optional but user-friendly)
You can define the list of bot commands (what users see when they type /
) either:
- In BotFather via /mybots → Edit Bot → Edit Commands, or
- Programmatically with
setMyCommands
using your library or raw Bot API.
Telegram’s official “From BotFather to Hello World” guide links to more advanced command handling and examples if you want to go deeper.
Deployment considerations
Any HTTPS-capable host works:
- A small VM (Ubuntu + systemd)
- Serverless (AWS Lambda/Cloud Functions) with webhook integration
- Containers on Fly.io/Render/Heroku-like platforms
Telegraf’s docs include examples for Lambda, GCF, Express, Fastify, etc.
Security & reliability checklist
- Keep the token secret (env vars, secrets manager). Revoke in BotFather if leaked.
- Use
secret_token
with webhooks and verify theX-Telegram-Bot-Api-Secret-Token
header. - Don’t mix polling and webhooks; choose one at a time.
- Handle errors & retries: Telegram will retry non-2xx webhook responses. Log appropriately.
- Be mindful of update types (messages, callbacks, inline queries, payments, etc.) and subscribe only to what you need (
allowed_updates
).
Working directly with the HTTP API (optional)
You can call endpoints like:
https://api.telegram.org/bot<token>/getMe
https://api.telegram.org/bot<token>/getUpdates
https://api.telegram.org/bot<token>/sendMessage
Use GET or POST with JSON or form data as per the spec.
What to do and read next
- Official Bot API reference: methods, objects, limits, formatting, payments, inline mode.
- Official “From BotFather to ‘Hello World’” guide: deeper walkthrough and multi-language examples.
- python-telegram-bot docs (stable): modern async patterns.
- Telegraf docs: quick recipes, webhook helpers, and TS types.
AWS deployment steps for the Python version
We have two main options for deployment of the Telegram bot to AWS infrastructure:
- A) Serverless (API Gateway + Lambda + Secrets Manager) — cheapest/easiest to run, great for modest traffic.
- B) Containerized (ECS Fargate + ALB + ACM) — production-grade for steady traffic and long-running libraries like
python-telegram-bot
in webhook mode.
A) Serverless on AWS (API Gateway + Lambda)
Use this when you want zero servers and near-zero idle cost. The code below handles Telegram webhooks directly (no long-running event loop).
- Prepare a minimal Lambda handler (Python)
Create handler.py
:
import json, os, urllib.request
BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
SECRET_HEADER = os.environ.get("WEBHOOK_SECRET", "") # must match Bot API setWebhook secret_token
API_BASE = f"https://api.telegram.org/bot{BOT_TOKEN}"
def reply(chat_id: int, text: str):
data = json.dumps({"chat_id": chat_id, "text": text}).encode()
req = urllib.request.Request(f"{API_BASE}/sendMessage", data=data, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req) as resp:
return resp.read()
def lambda_handler(event, context):
# 1) Verify Telegram's secret header (set when you configure the webhook)
if SECRET_HEADER:
if event.get("headers", {}).get("X-Telegram-Bot-Api-Secret-Token") != SECRET_HEADER:
return {"statusCode": 401, "body": "invalid secret"}
# 2) Parse update
body = event.get("body") or "{}"
update = json.loads(body)
message = update.get("message") or update.get("edited_message")
if not message:
# ignore non-message updates (callback_query, inline_query, etc.) for now
return {"statusCode": 200, "body": "ok"}
chat_id = message["chat"]["id"]
text = (message.get("text") or "").strip()
# 3) Simple routing
if text.startswith("/start"):
reply(chat_id, "👋 Hello from AWS Lambda! Send any text and I’ll echo it.")
elif text.startswith("/help"):
reply(chat_id, "/start – greet\n/help – help\n(Deployed on AWS Lambda)")
elif text:
reply(chat_id, text) # echo
return {"statusCode": 200, "body": "ok"}
This uses raw HTTPS calls to the Bot API to keep Lambda lean and cold-start-friendly. You can expand routing later.
- Package & deploy the Lambda
# Project layout
# .
# ├─ handler.py
# └─ requirements.txt # (leave empty for this minimal example)
zip -r function.zip handler.py
aws lambda create-function \
--function-name telegram-bot-webhook \
--runtime python3.11 \
--role arn:aws:iam::<ACCOUNT_ID>:role/<LambdaExecutionRole> \
--handler handler.lambda_handler \
--timeout 10 \
--memory-size 256 \
--zip-file fileb://function.zip \
--environment Variables='{TELEGRAM_BOT_TOKEN=<TOKEN_FROM_BOTFATHER>,WEBHOOK_SECRET=my-secret-42}'
IAM role (<LambdaExecutionRole>
) needs AWSLambdaBasicExecutionRole
for CloudWatch Logs.
Prefer storing your token in AWS Secrets Manager and loading it at init—this example uses env vars for brevity.
- Create an HTTPS endpoint (API Gateway)
# HTTP API (not REST) for lower latency
API_ID=$(aws apigatewayv2 create-api \
--name telegram-webhook \
--protocol-type HTTP \
--target arn:aws:lambda:<REGION>:<ACCOUNT_ID>:function:telegram-bot-webhook \
--query 'ApiId' --output text)
# Add a default route POST /webhook
aws apigatewayv2 create-integration \
--api-id $API_ID \
--integration-type AWS_PROXY \
--integration-uri arn:aws:lambda:<REGION>:<ACCOUNT_ID>:function:telegram-bot-webhook \
--payload-format-version 2.0
aws apigatewayv2 create-route \
--api-id $API_ID \
--route-key "POST /webhook" \
--target "integrations/$(
aws apigatewayv2 get-integrations --api-id $API_ID --query 'Items[0].IntegrationId' --output text
)"
aws apigatewayv2 create-deployment --api-id $API_ID
GW_URL=$(aws apigatewayv2 get-apis --query "Items[?ApiId=='$API_ID'].ApiEndpoint" --output text)
echo $GW_URL # e.g., https://abc123.execute-api.ap-somewhere.amazonaws.com
Ensure Lambda permission is auto-added; if not:
aws lambda add-permission \
--function-name telegram-bot-webhook \
--statement-id apigw \
--action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn "arn:aws:execute-api:<REGION>:<ACCOUNT_ID>:$API_ID/*/*/webhook"
- Point Telegram to your webhook
TOKEN=<TOKEN_FROM_BOTFATHER>
WEBHOOK_URL="$GW_URL/webhook"
SECRET="my-secret-42"
curl -X POST "https://api.telegram.org/bot${TOKEN}/setWebhook" \
-d url="${WEBHOOK_URL}" \
-d secret_token="${SECRET}" \
-d drop_pending_updates=true
Send /start
to your bot—messages will flow through API Gateway → Lambda.
- Logs, retries, and updates
- Watch logs:
aws logs tail /aws/lambda/telegram-bot-webhook --follow
- Telegram retries when your endpoint isn’t 2xx; keep responses fast.
- To roll out new code: re-zip +
aws lambda update-function-code --function-name telegram-bot-webhook --zip-file fileb://function.zip
.
B) Containerized python-telegram-bot
on ECS Fargate + ALB
Use this when you want the ergonomics of
python-telegram-bot
(PTB) with a proper async app and persistent connections. We’ll run PTB behind an HTTPS load balancer.
- App code (FastAPI + PTB webhook)
app.py
:
import os, asyncio
from fastapi import FastAPI, Request, Header
from telegram import Update
from telegram.ext import ApplicationBuilder, CommandHandler, MessageHandler, ContextTypes, filters
TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
SECRET = os.environ.get("WEBHOOK_SECRET", "")
app = FastAPI()
tg_app = None # PTB application
async def start_cmd(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text("🚀 Hello from ECS Fargate + PTB!")
async def echo(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
await update.message.reply_text(update.message.text)
@app.on_event("startup")
async def on_startup():
global tg_app
tg_app = ApplicationBuilder().token(TOKEN).build()
tg_app.add_handler(CommandHandler("start", start_cmd))
tg_app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
# No run_polling(); we’ll feed webhooks manually.
@app.post("/telegram/webhook")
async def telegram_webhook(request: Request, x_telegram_bot_api_secret_token: str | None = Header(default=None)):
if SECRET and (x_telegram_bot_api_secret_token != SECRET):
return {"ok": True} # ignore silently
data = await request.json()
update = Update.de_json(data, tg_app.bot)
await tg_app.process_update(update)
return {"ok": True}
uvicorn
entrypoint: uvicorn app:app --host 0.0.0.0 --port 8080
- Dockerfile
FROM python:3.11-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
RUN pip install --no-cache-dir fastapi uvicorn[standard] python-telegram-bot==21.*
COPY app.py .
EXPOSE 8080
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]
Build & push:
aws ecr create-repository --repository-name telegram-ptb || true
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION=<REGION>
ECR="$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/telegram-ptb"
aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin "$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com"
docker build -t telegram-ptb .
docker tag telegram-ptb:latest $ECR:latest
docker push $ECR:latest
- ECS Fargate service behind an ALB
- Create a Security Group allowing inbound 443 on the ALB; ECS tasks allow 8080 from the ALB SG.
- Create an Application Load Balancer (ALB) with an HTTPS listener (443) and an ACM certificate for your domain.
- Target group: HTTP 8080 (IP target type), health check path
/
(FastAPI serves 404; you can add@app.get("/")
health route).
Create a Task Definition (Fargate) with:
- Container image:
$ECR:latest
- Port mapping: 8080
- Env vars:
TELEGRAM_BOT_TOKEN
,WEBHOOK_SECRET
- Task role: basic CloudWatch logging.
Create an ECS Service:
- Launch type Fargate, desired count 1+.
- Attach to the ALB target group.
Note the public HTTPS domain of the ALB (or use Route 53 to point your DNS name).
- Tell Telegram your webhook URL
TOKEN=<TOKEN_FROM_BOTFATHER>
SECRET="my-secret-42"
WEBHOOK_URL="https://your.domain.com/telegram/webhook"
curl -X POST "https://api.telegram.org/bot${TOKEN}/setWebhook" \
-d url="${WEBHOOK_URL}" \
-d secret_token="${SECRET}" \
-d drop_pending_updates=true \
-d allowed_updates='["message","edited_message","callback_query"]'
- Scale & operate
- Scale ECS tasks up/down; ALB will spread traffic.
- Rotate tokens in Secrets Manager, update service with new task env.
- Use CloudWatch Logs for app logs and ALB access logs (S3).
Which should we pick?
- Lambda + API Gateway: simplest, cheapest at low volume; great for bots that do a few calls per minute.
- ECS Fargate + ALB: best when you want the full
python-telegram-bot
experience, custom middlewares, background jobs, and steady traffic.
Quick checklist (both approaches)
- Use HTTPS endpoint +
secret_token
and verify theX-Telegram-Bot-Api-Secret-Token
header. - Choose webhook OR polling (not both).
- Persist config in Secrets Manager, not in code.
- Add observability: CloudWatch Logs + metrics (5xx alarms).
- Handle only the update types you need; expand later.
Useful links
- https://docs.python-telegram-bot.org
- https://telegraf.js.org
- https://core.telegram.org/bots/tutorial
- https://core.telegram.org/bots/api
- AWS CDK Overview, TypeScript and Python Examples and Performance Conciderations
- AWS lambda performance: JavaScript vs Python vs Golang
- Layered Lambdas with AWS SAM and Python
- AWS SAM + AWS SQS + Python PowerTools
- Python Cheatsheet
- uv - New Python Package, Project, and Environment Manager
- Install Node.js