Telegram Bots

Hosting a pyTelegramBotAPI (telebot) Bot

Deploy a pyTelegramBotAPI (telebot) bot that stays online — reliable polling, threading pitfalls, error handling, inline keyboards and a clean production setup.

On this page
  1. Install and pin
  2. A minimal, production-minded bot
  3. Understand telebot’s threading
  4. Handle errors deliberately
  5. HTML parse mode and user input
  6. Conversation state
  7. Deploying
  8. Clean shutdowns
  9. Switching to webhooks later
  10. Frequently asked questions
  11. Summary

pyTelegramBotAPI — imported as telebot — is one of the oldest and most approachable Python libraries for Telegram bots. Its decorator style makes a working bot a dozen lines long. Running that bot reliably for months takes a few extra steps, especially around polling and threads. This guide covers them.

Install and pin

# requirements.txt
pyTelegramBotAPI>=4.20,<5

The package name on PyPI is pyTelegramBotAPI; the import is telebot. Pin the major version so a future release can’t change behaviour under you. Kerit Cloud supports Python 3.9–3.12, and current telebot releases work on all of them.

A minimal, production-minded bot

import logging
import os

import telebot
from telebot import types

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
telebot.logger.setLevel(logging.INFO)

bot = telebot.TeleBot(os.environ["BOT_TOKEN"], parse_mode="HTML")


@bot.message_handler(commands=["start", "help"])
def send_welcome(message: types.Message):
    bot.reply_to(message, "Hi! Try /menu.")


@bot.message_handler(commands=["menu"])
def menu(message: types.Message):
    kb = types.InlineKeyboardMarkup()
    kb.add(
        types.InlineKeyboardButton("Status", callback_data="status"),
        types.InlineKeyboardButton("About", callback_data="about"),
    )
    bot.send_message(message.chat.id, "Choose an option:", reply_markup=kb)


@bot.callback_query_handler(func=lambda c: c.data in {"status", "about"})
def on_button(call: types.CallbackQuery):
    text = "All systems normal." if call.data == "status" else "A bot hosted 24/7."
    bot.answer_callback_query(call.id)
    bot.send_message(call.message.chat.id, text)


if __name__ == "__main__":
    bot.infinity_polling(timeout=20, long_polling_timeout=30)

Two lines deserve attention.

answer_callback_query must be called for every button press. Without it, the user’s Telegram client shows a loading spinner on the button until it times out.

infinity_polling rather than polling. The plain polling() method stops when certain exceptions occur — a network hiccup can end your bot’s polling loop while the process stays alive, so it looks online but never answers. infinity_polling() catches errors, logs them and restarts polling automatically, which is what you want on a server.

Understand telebot’s threading

By default, TeleBot is threaded: handlers run in a small pool of worker threads so a slow handler doesn’t block others. That’s convenient, but it has consequences:

  • Shared state needs locking. If two handlers modify the same dictionary at once, results can be inconsistent. Use a threading.Lock or, better, keep state in a database.
  • SQLite connections are per-thread. Python’s sqlite3 refuses to use a connection from a different thread than the one that created it. Either open a connection inside each handler, or create the connection with check_same_thread=False and protect it with a lock.
  • Blocking is fine within reason. Because handlers run in threads, calling requests or doing short blocking work won’t freeze the whole bot — but a handful of very slow handlers can exhaust the pool. Increase it with TeleBot(token, num_threads=4) if needed.

If you prefer asyncio, telebot also ships an async client — from telebot.async_telebot import AsyncTeleBot — with the same decorator style and await bot.infinity_polling(). Pick one model and stick with it.

Handle errors deliberately

Exceptions inside handlers are logged by telebot, but you can centralise handling with an exception handler class:

class LogHandler(telebot.ExceptionHandler):
    def handle(self, exception):
        logging.exception("Unhandled error in handler", exc_info=exception)
        return True  # mark as handled


bot = telebot.TeleBot(os.environ["BOT_TOKEN"], parse_mode="HTML", exception_handler=LogHandler())

Expect certain API errors in normal operation and handle them explicitly:

from telebot.apihelper import ApiTelegramException

try:
    bot.send_message(user_id, text)
except ApiTelegramException as e:
    if e.error_code == 403:        # user blocked the bot
        mark_inactive(user_id)
    elif e.error_code == 429:      # too many requests
        retry_after = e.result_json.get("parameters", {}).get("retry_after", 5)
        schedule_retry(user_id, text, retry_after)
    else:
        raise

Telegram’s limits are covered in Telegram Bot API limits and how to stay under them.

HTML parse mode and user input

Setting parse_mode="HTML" makes formatting easy, but any user-supplied text you echo back must be escaped, or a stray < breaks the message with a “can’t parse entities” error:

import html
bot.reply_to(message, f"You said: <i>{html.escape(message.text)}</i>")

Conversation state

telebot supports simple step-by-step flows with register_next_step_handler, and a more structured state system with storage backends (memory or Redis). For production, prefer persistent storage — in-memory state is lost on every restart and redeploy. For small bots, a state column in a SQLite or MySQL users table is a simple, durable alternative. See storing Telegram bot state.

Deploying

  1. Create a server on Kerit Cloud’s free plan or a paid Telegram bot plan and pick a Python runtime.
  2. Upload bot.py and requirements.txt with the file manager or SFTP, or link your repository on a paid plan so every push redeploys.
  3. Add BOT_TOKEN as an environment variable — stored encrypted, never logged.
  4. Set the start command to python -u bot.py. The -u flag disables output buffering so log lines appear in the console immediately.
  5. Start the server and send /start to your bot.

A telebot bot using long polling typically idles well under 100 MB, so the free plan’s 256 MB is plenty for most. If the process crashes, the watchdog restarts it within seconds.

Warning: Stop any copy running on your own computer first. Two processes polling the same bot cause Error code: 409. Conflict: terminated by other getUpdates request.

Clean shutdowns

When the host restarts your bot, it sends SIGTERM. To stop polling and close resources cleanly, register a handler:

import signal
import sys

def shutdown(*_):
    bot.stop_polling()
    db.close()
    sys.exit(0)

signal.signal(signal.SIGTERM, shutdown)

Switching to webhooks later

If your bot grows and you want webhooks, telebot supports them through its built-in webhook listener or any web framework (Flask, FastAPI, aiohttp) that passes updates to bot.process_new_updates(). Every Kerit Cloud Telegram server includes an HTTPS endpoint you can register. The trade-offs are in long polling vs webhooks.

Frequently asked questions

Does telebot support webhooks and polling equally well? Yes. Polling with infinity_polling() is simplest and needs no domain. Webhooks suit high-volume bots and bots that already run a web server.

Why does my bot reply twice? Two copies are running — usually one on your computer. Only one process should serve a bot token.

How much memory does a telebot bot use? Small long-polling bots usually idle well under 100 MB. Memory grows with the thread pool, caches and anything you keep in dictionaries, so move persistent data to a database.

Can I run several telebot bots on one server? You can run them as separate processes. On Kerit Cloud, the Ultra plan explicitly supports multiple bots per server; on smaller plans, one bot per server gives each its own resources. See running multiple Telegram bots on one server.

Summary

A reliable telebot bot uses infinity_polling() so network errors never end the polling loop, answers every callback query, escapes user input in HTML mode, and respects the library’s threaded model — locking shared state or keeping it in a database. Add a central exception handler, handle 403 and 429 errors explicitly, stop polling on SIGTERM, and deploy with python -u bot.py on a host that keeps the process running.