Recommended resources
The freely available resources this lesson will direct you to.
In our previous lesson, you made your bot significantly more robust by implementing a global error boundary. It can now withstand internal runtime errors without crashing, which is a crucial step for any production-quality service. However, its resilience is not yet complete. The bot still reacts poorly to being shut down from the outside, for example, when you press Ctrl+C in your terminal or when a deployment service wants to restart the process.
This lesson focuses on addressing that final piece of the reliability puzzle: graceful shutdown. You will learn how to intercept termination signals sent to your bot's process and perform a clean shutdown procedure. For our long-polling bot, this means ensuring it stops its polling loop correctly, allowing any in-flight requests to complete before the process exits. This prevents abrupt disconnections and ensures your bot behaves like a well-mannered citizen in its runtime environment.
Why Graceful Shutdown Matters
When a long-running process like a server or a bot is terminated, it's rarely an instantaneous event. The operating system or orchestrator (like Docker or Kubernetes) typically first sends a "request" for the process to terminate. This gives the application a short window of opportunity to perform cleanup tasks. These tasks might include closing database connections, writing pending data to disk, or, in our case, telling the Telegram API that we are done fetching updates.
Ignoring this opportunity and letting the process be forcefully killed can lead to problems like resource leaks, data corruption, or leaving other systems in an inconsistent state. A graceful shutdown procedure ensures that your application terminates cleanly and predictably.
To understand this concept in a broader context, the following video provides an excellent overview of why graceful shutdown is a best practice in Node.js applications.
Here's how to Gracefully Shutdown your apps (with Node.js examples)
This video from the Software Developer Diaries channel explains the core principles of graceful shutdown.
Watch the first two sections, starting from the introduction which explains the "why," followed by the explanation of termination signals. Pay close attention to the distinction between SIGINT, SIGTERM, and SIGKILL.
As the video explains, our process can receive different signals. The two most important ones for us are:
SIGINT(Signal Interrupt): This is the signal sent when you pressCtrl+Cin your terminal. It's the manual "stop" command.SIGTERM(Signal Terminate): This is a more generic termination signal used by automated systems likesystemd, Docker, and Kubernetes to politely ask a service to stop.
Both SIGINT and SIGTERM can be "caught" by our application, allowing us to run custom logic before the process exits. SIGKILL, on the other hand, is the forceful "kill" signal that cannot be caught. Our goal is to handle SIGINT and SIGTERM properly so that a SIGKILL is never necessary.
Your experience managing systems likely involves interacting with processes. Tools like htop or kill are interfaces for sending exactly these kinds of signals.
Implementing Graceful Shutdown in grammY
Now, let's apply this to our grammY bot. When our bot is running via bot.start(), it's in a continuous long-polling loop, maintaining an open HTTP request to Telegram's servers waiting for updates. A graceful shutdown for us means telling grammY to stop this polling and let the current request finish cleanly.
The grammY framework provides a simple method for this: bot.stop(). When called, it stops the bot from making any new getUpdates requests.
The remaining task is to call this method when our process receives a SIGINT or SIGTERM signal. In a Bun or Node.js environment, we can listen for these process-level events using process.once().
The grammY documentation on reliability provides the exact pattern for this.
Scaling Up III: Reliability | grammY
This part of the grammY documentation explains how to handle termination for a long-polling bot.
Read the introduction and the section on Graceful Shutdown. Then, review the code snippet under Simple Long Polling. This is the pattern we will implement.
Following the documentation's guidance, let's modify your index.ts file. We need to add listeners for the SIGINT and SIGTERM events. These should be placed before the bot.start() call.
// index.ts
import { Bot, GrammyError, HttpError } from "grammy";
import "dotenv/config";
const token = process.env.TELEGRAM_BOT_TOKEN;
if (!token) throw new Error("TELEGRAM_BOT_TOKEN is not set!");
const bot = new Bot(token);
// Error handler (from previous lesson)
bot.catch((err) => {
const ctx = err.ctx;
console.error(`Error while handling update ${ctx.update.update_id}:`);
const e = err.error;
if (e instanceof GrammyError) {
console.error("Error in request:", e.description);
} else if (e instanceof HttpError) {
console.error("Could not contact Telegram:", e);
} else {
console.error("Unknown error:", e);
}
});
// Message handler
bot.on("message:text", (ctx) => {
if (ctx.message.text === "crash") {
throw new Error("This is a simulated crash!");
}
console.log(`Received message from ${ctx.from.first_name}: "${ctx.message.text}"`);
ctx.reply(`Echo: ${ctx.message.text}`);
});
// Add these listeners for graceful shutdown.
// We use 'once' because the event should only be handled once.
process.once("SIGINT", () => {
console.log("SIGINT received, stopping bot...");
bot.stop();
});
process.once("SIGTERM", () => {
console.log("SIGTERM received, stopping bot...");
bot.stop();
});
// Start the bot.
bot.start();
console.log("Bot has been started and is listening for messages...");
I've added console.log messages to our handlers. This makes it visually clear that our shutdown logic is being triggered when we stop the process.
Now, run your bot again with bun run index.ts. Let it start up, and then press Ctrl+C in your terminal.
You should see the "SIGINT received, stopping bot..." message, followed by the process exiting cleanly. There will be no error stack trace. The bot.stop() call cancels the long polling, and once that's done, the Bun process can terminate. It might take a moment, as grammY waits for the current long-polling request to time out or complete before the process exits.
You have now successfully implemented graceful shutdown. Your bot is not only resilient to internal errors but also a well-behaved citizen of its host environment, capable of handling external termination requests cleanly.
Conclusion
In this lesson, you've completed the foundational work to make your bot truly robust. By handling termination signals, you ensure that your bot can shut down cleanly, which is a professional standard for any long-running service.
Key Takeaways:
- Graceful shutdown is the process of cleaning up resources before an application terminates.
- Processes can receive signals like
SIGINT(fromCtrl+C) andSIGTERM(from orchestrators) to initiate termination. - In Bun/Node.js, you can catch these signals using
process.once('SIGINT', ...)andprocess.once('SIGTERM', ...). - For a simple long-polling grammY bot, the cleanup action is to call
bot.stop()to end the polling loop gracefully.
With a bot that is now both resilient and well-behaved, we can finally move on to building more interesting features. In the next lesson, we will begin exploring one of the most fundamental aspects of bot interaction: handling commands like /start and /help.