Implementing Telegram Bot in Python and Javascript with deployment to AWS

And deploying new Telegram bot to AWS

Page content

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.

robots working in IT

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

  1. Create a bot via @BotFather → get token.
  2. Build a small app with python-telegram-bot or Telegraf.
  3. 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

Create your bot with BotFather (get the token)

  1. In Telegram, search for @BotFather and start a chat.
  2. Send /newbot and follow the prompts to name your bot and choose a unique username (must end with bot), e.g. example_helper_bot.
  3. 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 supports secret_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). Use ApplicationBuilder().token(...).build(), expose a POST route that feeds incoming JSON updates into your application, and call bot.set_webhook(...). See the ApplicationBuilder docs for construction patterns. ([docs.python-telegram-bot.org][2])

Testing locally? Use a tunnel (e.g., ngrok) to expose https://... to Telegram, then call setWebhook 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 the X-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).

  1. 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.

  1. 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.

  1. 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"
  1. 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.

  1. 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.

  1. 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

  1. 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
  1. 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).

  1. 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"]'
  1. 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 the X-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.