SIGTERM and SIGINT Signals — Complete Implementation Guide
In this tutorial, you will learn about SIGTERM and SIGINT Signals. We cover key concepts, practical examples, and best practices to help you master this topic.
SIGTERM and SIGINT are the primary signals used to initiate graceful shutdown, with SIGTERM sent by orchestrators and process managers, and SIGINT sent by Ctrl+C in the terminal.
What You'll Learn
By the end of this tutorial, you will know how to register signal handlers, differentiate between signal types, perform cleanup operations, and ensure the process exits cleanly.
Why It Matters
Handling signals correctly is the foundation of graceful shutdown. If your application doesn't respond to SIGTERM, Kubernetes cannot manage it properly, and manual shutdown becomes unpredictable.
Real-World Use
DodaTech's containerized Microservices all implement the same signal handler pattern: catch SIGTERM, start shutdown sequence, force exit after 25 seconds (5 seconds before Kubernetes' 30-second limit).
Signal Handling Learning Path
flowchart LR
A[Why Graceful Shutdown] --> B[SIGTERM and SIGINT]
B --> C[Registering Handlers]
B --> D[Cleanup Sequences]
B --> E[Force Exit]
B --> F{You Are Here}
style F fill:#f9f,color:#fff
Understanding the Signals
| Signal | Number | Source | Default Behavior |
|---|---|---|---|
| SIGTERM | 15 | Kubernetes, systemd, Docker | Terminate |
| SIGINT | 2 | Ctrl+C in terminal | Terminate |
| SIGHUP | 1 | Terminal closed, reload | Terminate |
| SIGQUIT | 3 | Ctrl+\ | Terminate with core dump |
Registering Signal Handlers in Node.js
Node.js provides the process.on() method to register handlers for POSIX signals.
function setupSignalHandlers() {
const shutdownTasks = [];
function registerCleanupTask(name, task) {
shutdownTasks.push({ name, task });
}
async function handleSignal(signal) {
console.log(`\n[${new Date().toISOString()}] Received ${signal}`);
console.log(`Starting ${shutdownTasks.length} cleanup tasks...`);
for (const { name, task } of shutdownTasks) {
try {
console.log(`Running: ${name}`);
await task();
console.log(`Completed: ${name}`);
} catch (err) {
console.error(`Failed: ${name}`, err.message);
}
}
console.log("All cleanup tasks complete, exiting");
process.exit(0);
}
process.on("SIGTERM", () => handleSignal("SIGTERM"));
process.on("SIGINT", () => handleSignal("SIGINT"));
process.on("SIGHUP", () => handleSignal("SIGHUP"));
return { registerCleanupTask };
}
const handler = setupSignalHandlers();
handler.registerCleanupTask("Close DB pool", () => Promise.resolve());
handler.registerCleanupTask("Save cache", () => Promise.resolve());
// [2026-06-28T12:00:00.000Z] Received SIGTERM
// Starting 2 cleanup tasks...
// Running: Close DB pool
// Completed: Close DB pool
// Running: Save cache
// Completed: Save cache
// All cleanup tasks complete, exiting
Signal Handling in Python
Python uses the signal module but requires careful handling since signal handlers don't work well with asyncio.
import signal
import sys
import asyncio
class GracefulShutdown:
def __init__(self):
self.shutdown_requested = False
self.cleanup_tasks = []
def add_cleanup_task(self, name, coro):
self.cleanup_tasks.append((name, coro))
def handle_signal(self, sig, frame):
print(f"Received {signal.Signals(sig).name}")
self.shutdown_requested = True
async def shutdown(self):
print(f"Starting {len(self.cleanup_tasks)} cleanup tasks...")
for name, coro in self.cleanup_tasks:
try:
print(f"Running: {name}")
await coro
print(f"Completed: {name}")
except Exception as e:
print(f"Failed: {name}: {e}")
print("Shutdown complete")
async def main():
shutdown_handler = GracefulShutdown()
signal.signal(signal.SIGTERM, shutdown_handler.handle_signal)
signal.signal(signal.SIGINT, shutdown_handler.handle_signal)
shutdown_handler.add_cleanup_task("Close DB", asyncio.sleep(0.1))
shutdown_handler.add_cleanup_task("Close Redis", asyncio.sleep(0.1))
while not shutdown_handler.shutdown_requested:
await asyncio.sleep(1)
await shutdown_handler.shutdown()
asyncio.run(main())
# Received SIGTERM
# Starting 2 cleanup tasks...
# Running: Close DB
# Completed: Close DB
# Running: Close Redis
# Completed: Close Redis
# Shutdown complete
Setting a Force Exit Timeout
Signal handlers must include a safety timeout to prevent the process from hanging indefinitely.
function withTimeout(shutdownFn, timeoutMs = 25000) {
let timeoutHandle;
const timeoutPromise = new Promise((_, reject) => {
timeoutHandle = setTimeout(() => {
console.error(`Shutdown timed out after ${timeoutMs}ms, forcing exit`);
process.exit(1);
}, timeoutMs);
if (timeoutHandle.unref) timeoutHandle.unref();
});
return Promise.race([
shutdownFn(),
timeoutPromise
]).finally(() => clearTimeout(timeoutHandle));
}
function createSafeShutdown() {
return {
async shutdown() {
console.log("Starting graceful shutdown");
await withTimeout(Promise.all([
new Promise(r => setTimeout(r, 5000)),
new Promise(r => setTimeout(r, 3000))
]), 10000);
console.log("Shutdown clean");
}
};
}
const safe = createSafeShutdown();
safe.shutdown();
// Starting graceful shutdown
// Shutdown clean
Common Mistakes
Using process.on("exit") instead of process.on("SIGTERM") -- The exit event fires when the process is already exiting. You cannot perform async cleanup there. Use SIGTERM handler for cleanup.
Not calling process.exit() at the end -- After cleanup, the process may not exit if there are active handles (timers, connections). Call process.exit() or use { once: true } on the signal listener.
Registering multiple handlers for the same signal -- Only the last registered handler runs. Use a single handler that calls all cleanup functions sequentially.
Blocking the event loop in signal handlers -- Signal handlers run synchronously. Don't use synchronous I/O or long loops. Offload work to async functions.
Not handling SIGTERM in child processes -- If using cluster or child_process, each process must handle signals independently. The parent cannot clean up child resources.
Practice Questions
What signal does Kubernetes send to initiate shutdown? SIGTERM. The pod has terminationGracePeriodSeconds (default 30) to exit before SIGKILL.
Why do signal handlers need to be async when using async/await? Signal handlers can be async but you must not block the event loop. Use async functions and await Promise.all for parallel cleanup.
What is the difference between unref() and ref() on timers in Node.js? unref() prevents a timer from keeping the process alive. Use unref() on shutdown timeout timers so they don't prevent exit.
Challenge: Implement a signal handler that prints a progress bar for each cleanup step.
class ProgressShutdown {
constructor() {
this.tasks = [];
}
addTask(name, fn) {
this.tasks.push({ name, fn });
}
async shutdown() {
const total = this.tasks.length;
for (let i = 0; i < total; i++) {
const { name, fn } = this.tasks[i];
const bar = "#".repeat(i + 1) + "-".repeat(total - i - 1);
process.stdout.write(`\r[${bar}] ${name}`);
await fn();
}
console.log("\nShutdown complete");
process.exit(0);
}
}
const ps = new ProgressShutdown();
ps.addTask("DB", () => new Promise(r => setTimeout(r, 500)));
ps.addTask("Cache", () => new Promise(r => setTimeout(r, 500)));
ps.addTask("Queue", () => new Promise(r => setTimeout(r, 500)));
ps.shutdown();
FAQ
Mini Project
Build a signal handler module that supports multiple cleanup tasks with progress reporting, timeout protection, and event logging.
class SignalManager {
constructor(options = {}) {
this.tasks = [];
this.timeout = options.timeout || 25000;
this.setup();
}
setup() {
process.on("SIGTERM", () => this.shutdown("SIGTERM"));
process.on("SIGINT", () => this.shutdown("SIGINT"));
process.on("uncaughtException", (err) => {
console.error("Uncaught exception:", err);
this.shutdown("ERROR");
});
}
task(name, fn) {
this.tasks.push({ name, fn });
}
async shutdown(source) {
console.log(`[${new Date().toISOString()}] Shutdown via ${source}`);
const timer = setTimeout(() => {
console.error("Timeout, force exit");
process.exit(1);
}, this.timeout).unref();
for (const task of this.tasks) {
try {
console.log(` ${task.name}...`);
await task.fn();
console.log(` ${task.name} ok`);
} catch (err) {
console.error(` ${task.name} failed:`, err.message);
}
}
clearTimeout(timer);
process.exit(0);
}
}
const sm = new SignalManager({ timeout: 10000 });
sm.task("close-http", () => Promise.resolve());
sm.task("close-db", () => Promise.resolve());
sm.task("flush-logs", () => Promise.resolve());
What's Next
Now that you understand signal handling, learn how to drain active connections during shutdown. Then explore handling in-flight requests during the shutdown window.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro