Building a Production Ready Telegram Bot

Building a Production Ready Telegram Bot

Building a scalable, production-ready Telegram bot requires far more than spinning up a script that polls the getUpdates API on your local laptop. When you run a bot in production that handles thousands of concurrent users, you need low latency, resilient state management, automated crash recovery, strict webhook authentication, and seamless workflow integration.

In this guide, I will take you step-by-step through building a robust, production-grade Telegram bot architecture. We will cover choosing between webhooks and long polling, writing a complete Express server with request validation, integrating with PostgreSQL for state management, linking your bot to n8n Cloud for automated workflow execution, and hardening your server deployment.

What You’ll Need

  • n8n Cloud or self-hosted n8n instance
  • Hetzner VPS or Contabo VPS running Ubuntu 22.04 LTS
  • DigitalOcean as an alternative cloud provider
  • Namecheap domain configured with a valid SSL certificate
  • Node.js LTS (v20+) installed on your local environment and deployment server
  • Make.com (optional reference for visual workflow comparisons)

Table of Contents


Architectural Choices: Webhooks vs. Long Polling

When developing Telegram bots, you have two mechanisms for receiving updates: long polling and webhooks.

Long polling forces your application to open a persistent connection to Telegram’s servers and repeatedly pull updates. While convenient during early local development, it fails to scale under high traffic and introduces unwanted latency. Webhooks, on the other hand, push updates to your server instantly via HTTP POST requests whenever a user interacts with your bot.

For a production environment, webhooks are mandatory. However, accepting incoming public webhooks introduces security risks. You must verify that incoming requests strictly originate from Telegram and ensure your server processes events asynchronously so you don’t block Telegram’s webhook delivery system. If your server takes longer than a few seconds to return a 200 OK response, Telegram will assume the delivery failed and repeatedly retry, causing duplicate message handling.

To deploy a high-performance system affordably, hosting your bot on a dedicated Hetzner VPS or DigitalOcean droplet gives you total control over CPU resources and network IO. Deploying open-source automation infrastructure can significantly help you reduce your SaaS bill with self-hosted alternatives compared to paying per-execution SaaS tiers.

Let’s begin by acquiring a bot token from Telegram’s @BotFather. Open Telegram, search for @BotFather, start a chat, and send /newbot. Follow the prompts to set your bot’s display name and username. Save the HTTP API token securely; we will use it throughout this build.


💡 Fast-Track Your Project: Don’t want to configure this yourself? I build custom n8n pipelines and bots. Message me with code SYS3-HUGO.


Building the Production Express Webhook Server

We will now build a fully functional, production-ready Node.js webhook server using Express and PostgreSQL. This server validates secret tokens sent by Telegram, parses incoming messages, records audit logs into PostgreSQL, routes complex commands to external workflow engine handlers, and returns immediate response headers to satisfy Telegram’s HTTP constraints.

First, initialize your project and install the necessary dependencies in your terminal:

mkdir telegram-bot-service
cd telegram-bot-service
npm init -y
npm install express pg dotenv axios body-parser

Create a .env file to store environment variables safely:

PORT=3000
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyZ
WEBHOOK_SECRET_TOKEN=a_very_long_secure_random_string_32_chars
DATABASE_URL=postgres://botuser:securepassword@127.0.0.1:5432/telegram_bot_db
N8N_WEBHOOK_URL=https://n8n.yourdomain.com/webhook/telegram-ingest

Now, create server.js. Below is the complete application code. Every line is explicitly written without truncations or missing logic:

require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const { Pool } = require('pg');
const axios = require('axios');

const app = express();
app.use(bodyParser.json());

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

pool.on('error', (err) => {
  console.error('Unexpected error on idle PostgreSQL client', err);
});

async function logIncomingMessage(telegramId, username, messageText, payload) {
  const query = `
    INSERT INTO bot_logs (telegram_id, username, message_text, raw_payload, created_at)
    VALUES ($1, $2, $3, $4, NOW())
    RETURNING id;
  `;
  const values = [telegramId, username || null, messageText || '', JSON.stringify(payload)];
  const client = await pool.connect();
  try {
    const res = await client.query(query, values);
    return res.rows[0].id;
  } finally {
    client.release();
  }
}

async function sendTelegramMessage(chatId, text) {
  const url = `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`;
  try {
    await axios.post(url, {
      chat_id: chatId,
      text: text,
      parse_mode: 'HTML',
    });
  } catch (error) {
    console.error('Error sending message to Telegram API:', error.response ? error.response.data : error.message);
  }
}

app.post('/webhook/telegram', async (req, res) => {
  const secretHeader = req.headers['x-telegram-bot-api-secret-token'];
  if (secretHeader !== process.env.WEBHOOK_SECRET_TOKEN) {
    console.warn('Unauthorized webhook request attempt detected.');
    return res.status(403).send('Forbidden');
  }

  res.status(200).send('OK');

  const update = req.body;
  if (!update || !update.message) {
    return;
  }

  const message = update.message;
  const chatId = message.chat.id;
  const telegramId = message.from.id;
  const username = message.from.username;
  const text = message.text || '';

  try {
    await logIncomingMessage(telegramId, username, text, update);

    if (text.startsWith('/start')) {
      await sendTelegramMessage(chatId, 'Welcome! Your account has been registered. Send /help for instructions.');
    } else if (text.startsWith('/help')) {
      await sendTelegramMessage(chatId, 'Available commands:\n/start - Initialize registration\n/help - Show this guide\n/status - Check automated agent status');
    } else if (text.startsWith('/status')) {
      await sendTelegramMessage(chatId, 'System Status: 🟢 Operational\nDatabase: Connected\nWorkflow Engine: Active');
    } else {
      if (process.env.N8N_WEBHOOK_URL) {
        await axios.post(process.env.N8N_WEBHOOK_URL, {
          chat_id: chatId,
          telegram_id: telegramId,
          username: username,
          message_text: text,
          timestamp: new Date().toISOString()
        });
      } else {
        await sendTelegramMessage(chatId, `Received your message: "${text}"`);
      }
    }
  } catch (err) {
    console.error('Error processing background webhook job:', err);
  }
});

app.get('/health', async (req, res) => {
  try {
    const dbRes = await pool.query('SELECT 1');
    if (dbRes.rowCount === 1) {
      return res.status(200).json({ status: 'healthy', database: 'connected' });
    }
  } catch (err) {
    return res.status(500).json({ status: 'unhealthy', database: err.message });
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server executing successfully on port ${PORT}`);
});

Notice how we validate incoming authorization headers before processing data. Implement exact signature mechanisms when securing incoming webhooks with HMAC signature verification to protect public endpoints against injection or spoofing attacks.


Integrating Workflow Automation with n8n

While Express handles fast input validation and messaging routing, complex multi-step processing—like querying AI models, parsing PDFs, or connecting with CRMs—is far easier to manage via an orchestration platform like n8n Cloud .

When our Express backend receives a standard chat message, it passes the payload directly to an n8n webhook URL.

Setting Up the n8n Workflow

  1. Log into your n8n Cloud dashboard or self-hosted instance.
  2. Create a new workflow named Telegram Message Processor.
  3. Add a Webhook node set to HTTP Method POST and path telegram-ingest.
  4. Add an HTTP Request node connected to the Webhook node to send the reply back to Telegram’s API directly.

Below is the complete, raw n8n JSON workflow snippet. You can copy and paste this directly into your n8n workflow editor canvas:

{
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "telegram-ingest",
        "options": {}
      },
      "id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
      "name": "Webhook Ingest",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1,
      "position": [
        250,
        300
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"chat_id\": \"{{ $json.body.chat_id }}\",\n  \"text\": \"🤖 *Automated Workflow Response*\\n\\nWe received: \\\"{{ $json.body.message_text }}\\\"\\nProcessed user: @{{ $json.body.username }}\",\n  \"parse_mode\": \"Markdown\"\n}",
        "options": {}
      },
      "id": "9f8e7d6c-5b4a-3f2e-1d0c-9b8a7f6e5d4c",
      "name": "Reply via Telegram API",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [
        500,
        300
      ]
    }
  ],
  "connections": {
    "Webhook Ingest": {
      "main": [
        [
          {
            "node": "Reply via Telegram API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

This decoupled architecture lets your primary Express application run lean and stable, while n8n Cloud handles external integration retries, logic branches, and data mappings dynamically.


Database Persistence, Hardening, and Deployment

To run this stack in production, you must configure a PostgreSQL database, set up a Systemd daemon on your server to manage your Node.js process, install Caddy or Nginx for SSL handling, and register your public webhook with Telegram.

1. Database Setup

Log into your server instance (hosted via Hetzner VPS or Contabo VPS ) and run the following script to instantiate PostgreSQL schema tables and performance indexes:

CREATE DATABASE telegram_bot_db;

\c telegram_bot_db;

CREATE TABLE IF NOT EXISTS bot_logs (
    id SERIAL PRIMARY KEY,
    telegram_id BIGINT NOT NULL,
    username VARCHAR(255),
    message_text TEXT,
    raw_payload JSONB NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_bot_logs_telegram_id ON bot_logs(telegram_id);
CREATE INDEX idx_bot_logs_created_at ON bot_logs(created_at);

As your bot_logs table grows into millions of records, optimizing queries through targeted indexing becomes essential. Check out our detailed guide on optimizing database performance to maintain sub-millisecond response execution speeds.

2. Systemd Process Management

To ensure your Express service restarts automatically after system reboots or unhandled process crashes, create a custom Systemd unit file on your VPS at /etc/systemd/system/telegram-bot.service:

[Unit]
Description=Production Telegram Bot Express Service
After=network.target postgresql.service

[Service]
Type=simple
User=root
WorkingDirectory=/var/www/telegram-bot-service
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=5
Environment=NODE_ENV=production

[Install]
WantedBy=multi-user.target

Enable and launch the service:

sudo systemctl daemon-reload
sudo systemctl enable telegram-bot
sudo systemctl start telegram-bot
sudo systemctl status telegram-bot

3. Registering the Webhook with Telegram

Once your domain configured at Namecheap points to your server and serves traffic over HTTPS, issue a final curl call to register your production webhook endpoint directly with Telegram:

curl -X POST "https://api.telegram.org/bot123456789:ABCdefGHIjklMNOpqrsTUVwxyZ/setWebhook" \
     -H "Content-Type: application/json" \
     -d '{
           "url": "https://bot.yourdomain.com/webhook/telegram",
           "secret_token": "a_very_long_secure_random_string_32_chars",
           "drop_pending_updates": false
         }'

If successful, Telegram will respond with:

{
  "ok": true,
  "result": true,
  "description": "Webhook was set"
}

Getting Started

Building resilient, scalable automated interfaces doesn’t have to be complicated when built on solid foundation blocks:

  1. Claim your target domain using Namecheap .
  2. Provision a dedicated node through Hetzner VPS , Contabo VPS , or DigitalOcean .
  3. Connect visual backend orchestration pipelines utilizing n8n Cloud .

By offloading heavy async operations to specialized engine workers and keeping your core ingress webhook lightweight, secure, and performant, your Telegram bot will easily scale to thousands of users effortlessly.

Outsource Your Automation

Don’t have time? I build production n8n workflows, WhatsApp bots, and fully automated YouTube Shorts pipelines. Hire me on Fiverr — mention SYS3-HUGO for priority. Or DM at chasebot.online .

Want to automate this yourself?

Start with n8n Cloud (free tier available) or self-host on a Hetzner VPS for full control.

Want this engine running on your own VPS?

This blog publishes itself — daily, unattended, on free API tiers. The full engine, Hugo theme, and setup guide are available as System 3.

Get System 3
system online