EQVPS

Run a Telegram bot 24/7 on a VPS: aiogram, systemd, polling vs webhook

Jul 4, 2026 · 5 min read · EQVPS Team

You wrote a Telegram bot with aiogram, tested it locally, and it works. Now it has to live somewhere that doesn't sleep when you close the laptop. A small VPS is the natural home — but there's a gap between python bot.py in an SSH session and a bot that actually stays up through crashes, reboots, and your connection dropping.

This is the production version: a virtualenv, the token kept out of your code, a systemd service that resurrects the bot on its own, and a clear-eyed answer to the question everyone eventually asks — polling or webhook?

Why not just run it on your laptop

A bot needs a steady outbound connection to Telegram. Your laptop sleeps, reboots for updates, and hops between networks — every one of those drops the bot, and users hit a wall of silence. A VPS holds that connection around the clock. That's the whole reason to move it off your machine, and it's the same reason a Discord bot belongs on a server too.

If you just want the fastest possible "get it online" path with a minimal bot, the host-a-Telegram-bot walkthrough covers that. This piece goes a level deeper: aiogram, safe token handling, and knowing when to scale.

The bot, in a virtualenv

SSH in and keep the bot isolated in its own venv — don't install packages system-wide, it makes upgrades and cleanup a mess later:

sudo apt update && sudo apt install -y python3-venv
mkdir ~/tgbot && cd ~/tgbot
python3 -m venv venv && source venv/bin/activate
pip install -U aiogram

A minimal aiogram 3 bot that reads its token from the environment, not from a hard-coded string:

# bot.py
import asyncio, logging, os
from aiogram import Bot, Dispatcher
from aiogram.types import Message
from aiogram.filters import CommandStart

logging.basicConfig(level=logging.INFO)
dp = Dispatcher()

@dp.message(CommandStart())
async def start(m: Message):
    await m.answer("Alive and running on a VPS.")

async def main():
    bot = Bot(os.environ["BOT_TOKEN"])
    await dp.start_polling(bot)

if __name__ == "__main__":
    asyncio.run(main())

Keep the token out of your code

Never paste the BotFather token into bot.py — one push to a public repo and it's leaked. Put it in a root-readable env file instead:

sudo tee /etc/tgbot.env >/dev/null <<'EOF'
BOT_TOKEN=123456:your-token-from-botfather
EOF
sudo chmod 600 /etc/tgbot.env

A leaked token is one /revoke in BotFather away from being fixed — but a leaked anything else on a shared machine is worse. A dedicated VPS for the bot is honestly a safer home for the token than your daily laptop: if it leaks, you rotate one token, not your whole setup.

The part that keeps it alive: systemd

This is what separates a bot that runs from a bot that stays running. Create the service:

# /etc/systemd/system/tgbot.service
[Unit]
Description=Telegram bot (aiogram)
After=network-online.target
Wants=network-online.target

[Service]
User=botuser
WorkingDirectory=/home/botuser/tgbot
EnvironmentFile=/etc/tgbot.env
ExecStart=/home/botuser/tgbot/venv/bin/python bot.py
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target

Run it as a non-root user (botuser above), not root — if the bot is ever compromised, you want it boxed in. Then:

sudo systemctl daemon-reload
sudo systemctl enable --now tgbot

Restart=always with RestartSec=3 means a crash — a bad update from Telegram, an unhandled exception, an OOM — brings the bot back in three seconds instead of leaving it dead until you notice. Watch it live:

journalctl -u tgbot -f

That's your whole logging setup. No log files to rotate, no extra tooling — journald already has it.

Updating without downtime drama

When you change the code or bump aiogram:

cd ~/tgbot && source venv/bin/activate
pip install -U aiogram          # if upgrading the library
sudo systemctl restart tgbot
journalctl -u tgbot -n 30 --no-pager   # confirm it came back clean

The restart blips the bot for a second or two. For a polling bot that's invisible to users — Telegram queues updates and delivers them once the bot reconnects.

Polling vs webhook — the honest version

This is the decision people overthink. Here's the plain version:

Polling (start_polling, what the code above uses) has the bot ask Telegram "anything new?" on a long-lived connection. It needs nothing but outbound internet — no domain, no TLS, no open inbound ports. It runs fine behind NAT. For the overwhelming majority of bots, this is correct and you should stop here.

Webhook has Telegram push updates to you, which means you must expose a public HTTPS endpoint. That requires a domain and a reachable inbound port — so either a dedicated public IP or a reverse proxy in front. More setup, more things that break. The payoff is lower latency and less overhead at large scale — thousands of concurrent users, heavy update volume.

The practical rule: start with polling on a NAT plan. Move to a webhook only when you've actually outgrown polling — and if you do, that's when a dedicated IP earns its place, because you need that inbound HTTPS endpoint.

What it costs to run

A polling bot is light. It idles on Telegram's long-poll and reacts to messages, so the box mostly waits:

Signup is email-only and you pay in USDC or USDT on Base or Ethereum — no card, no ID. A card works too, but the on-ramp has a ~$27 minimum, so for a $3 plan it's smoother to top up a small balance once and let renewals draw from it. It's CPU-only, one datacenter in Germany — a non-issue for a bot, worth knowing if you needed a GPU or a specific region.

The honest bottom line

A Telegram bot is one of the cheapest things you can self-host: a $3 box, a systemd unit, and polling gets you a bot that's online through crashes and reboots without you babysitting it. Reach for a webhook and a bigger plan only when scale actually forces it — not because a tutorial told you webhooks are "better." Get the small box, keep the token in an env file, let systemd handle the uptime, and — before anything else — run the new-VPS security checklist so the box itself is locked down.

FAQ

Polling or webhook — which should I use?

Start with polling. It works from any box with outbound internet — no domain, no open ports, no TLS — and it's plenty for most bots. Switch to a webhook only when you're running at scale (thousands of users) or need the lowest possible latency; a webhook needs a public HTTPS endpoint, which means a domain and a reachable inbound port (a dedicated IP, or a reverse proxy), so it's more moving parts. For 95% of bots, polling on a small VPS is the right answer.

Do I need a dedicated IP or a domain for a Telegram bot?

Not for polling — the bot only makes outbound calls to Telegram, so a NAT plan with port-forwarded SSH is enough. You need a public HTTPS endpoint (domain + inbound port, i.e. a dedicated IP or reverse proxy) only if you switch to webhook mode. Most bots never need it.

What size VPS does an aiogram bot need?

The smallest. A polling bot mostly waits on Telegram's long-poll, so 1 vCPU / 1 GB (a $3 Nano) handles most bots comfortably. Size up only when the bot itself does the heavy lifting — a database, media processing, or a local model — which pushes you toward 2 GB ($5) or more.

How do I keep the bot running after I log out?

Run it as a systemd service with Restart=always. Launched in your terminal it dies when SSH closes; under systemd it survives logout, restarts on crash, and comes back after a reboot. That's the line between a demo and something you can rely on.

← Back to blogSee plans & pricing →