Hosting a Telegraf Bot on Node.js
Deploy a Telegraf 4 Telegram bot to production — filters, error handling, sessions, graceful stops, webhooks and the launch quirks that catch people out.
On this page
Telegraf is one of the longest-standing Node.js frameworks for Telegram bots, with a middleware design that will feel familiar if you’ve used Express or Koa. This guide takes a Telegraf 4 bot from a working script to a production deployment.
Install
npm init -y
npm install telegraf
package.json essentials:
{
"main": "bot.js",
"scripts": { "start": "node bot.js" },
"engines": { "node": ">=18" },
"dependencies": { "telegraf": "^4.16.0" }
}
Kerit Cloud supports Node.js 16 through 22. Telegraf 4 works on current LTS releases; use 20 or 22.
A production-ready bot
const { Telegraf } = require('telegraf');
const { message } = require('telegraf/filters');
const bot = new Telegraf(process.env.BOT_TOKEN);
bot.start((ctx) => ctx.reply('Welcome! Send /help for commands.'));
bot.help((ctx) => ctx.reply('Commands: /start, /help, /id'));
bot.command('id', (ctx) => ctx.reply(`Your ID: ${ctx.from.id}`));
bot.on(message('text'), (ctx) => ctx.reply(`You said: ${ctx.message.text}`));
bot.catch((err, ctx) => {
console.error(`Error while handling ${ctx.updateType} (update ${ctx.update.update_id}):`, err);
});
bot.launch(() => console.log('Bot is running'));
process.once('SIGINT', () => bot.stop('SIGINT'));
process.once('SIGTERM', () => bot.stop('SIGTERM'));
Filters
message('text') from telegraf/filters narrows updates to text messages and gives you correct types in TypeScript. There are filters for photos, documents, stickers and more, and callbackQuery('data') for button presses. Older tutorials use bot.on('text', ...); the filter style is the current recommendation.
Error handling
Without bot.catch, an exception in a handler propagates and can stop the bot. With it, errors are logged and the bot carries on. Log the update type and ID so you can reproduce problems.
The launch quirk
In recent Telegraf 4 releases, the promise returned by bot.launch() only settles when the bot stops — polling is a loop that runs until you stop it. So this never prints:
await bot.launch();
console.log('started'); // never reached while the bot is running
Pass a callback to launch() instead, as in the example above, or log before calling it.
Graceful stop
The SIGTERM handler matters on a server. Hosts send SIGTERM on restarts and redeploys; bot.stop() ends polling cleanly so the last batch of updates is confirmed and not processed twice after the restart.
Sessions and scenes
Telegraf’s built-in session() middleware stores data in memory, so it’s lost on every restart. For production, use a persistent store from the @telegraf/session package, which provides adapters for Redis, SQLite, PostgreSQL, MongoDB and others:
const { session } = require('telegraf');
const { Redis } = require('@telegraf/session/redis');
const store = Redis({ url: process.env.REDIS_URL });
bot.use(session({ store, defaultSession: () => ({ visits: 0 }) }));
For multi-step flows, Telegraf’s Scenes (with Scenes.WizardScene and a Stage) guide users through a sequence of steps. Scenes rely on sessions — another reason to make sessions persistent. Kerit Cloud’s Ultra plan includes Redis; on other plans, a SQLite or MySQL-backed store works well. Background on the choices is in storing Telegram bot state.
Keyboards and callbacks
const { Markup } = require('telegraf');
const { callbackQuery } = require('telegraf/filters');
bot.command('menu', (ctx) =>
ctx.reply('Pick one:', Markup.inlineKeyboard([
Markup.button.callback('Status', 'status'),
Markup.button.callback('About', 'about'),
]))
);
bot.action('status', async (ctx) => {
await ctx.answerCbQuery(); // stops the loading spinner
await ctx.reply('All systems normal.');
});
Always call answerCbQuery() for callback queries, even when you have nothing to say, or the button spins until it times out.
Webhooks
Polling is the default and needs no infrastructure. For webhooks, Telegraf can run its own server:
bot.launch({
webhook: {
domain: process.env.WEBHOOK_DOMAIN, // e.g. your-endpoint.example
port: Number(process.env.PORT) || 8080,
hookPath: '/telegram',
secretToken: process.env.WEBHOOK_SECRET,
},
});
Or mount it in an existing Express app with app.use(await bot.createWebhook({ domain, path: '/telegram' })). Every Kerit Cloud Telegram server includes an HTTPS endpoint you can use. For the reasoning behind each mode, see long polling vs webhooks.
Handling Telegram errors
Expect a few API errors in normal operation:
- 403 “bot was blocked by the user” — mark the user inactive and stop messaging them.
- 429 “Too Many Requests” — wait
err.parameters.retry_afterseconds before retrying. - 400 “message is not modified” — you tried to edit a message with identical content; safe to ignore.
const { TelegramError } = require('telegraf');
try {
await ctx.telegram.sendMessage(userId, text);
} catch (err) {
if (err instanceof TelegramError && err.code === 403) await markInactive(userId);
else throw err;
}
Limits and broadcasting patterns are covered in Telegram Bot API limits.
Deploy on Kerit Cloud
- Add
node_modules/and.envto.gitignoreand push to GitHub. - Create a server on the free plan or a paid Telegram bot plan, with a Node.js 20 or 22 runtime.
- Upload the project, or link the repository on a paid plan for automatic deploys on every push.
- Add
BOT_TOKEN(plusREDIS_URLor webhook variables if used) as environment variables. - Set the start command to
npm startand start the server.
Watch the console for “Bot is running”, then send /start. If the process crashes, the watchdog restarts it within seconds; with bot.catch in place, most errors won’t crash it at all.
Troubleshooting
409: Conflict: terminated by other getUpdates request — another copy is polling. Stop the local one.
409: Conflict: can't use getUpdates method while webhook is active — delete the webhook with bot.telegram.deleteWebhook() before polling.
Nothing logs after await bot.launch() — expected; see the launch quirk above.
Session data disappears on restart — you’re using the in-memory session store. Switch to a persistent one.
Summary
A production Telegraf bot uses the filter API, registers bot.catch so errors don’t stop it, logs startup through the launch() callback, stops cleanly on SIGTERM, and keeps sessions in a persistent store. Deploy it with npm start and a BOT_TOKEN variable, handle 403 and 429 errors deliberately, and switch to webhooks only when your traffic calls for it.