Skip to content

SIGTERM and SIGINT Signals — Complete Implementation Guide

DodaTech Updated 2026-06-28 6 min read

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

  1. 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.

  2. 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.

  3. Registering multiple handlers for the same signal -- Only the last registered handler runs. Use a single handler that calls all cleanup functions sequentially.

  4. 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.

  5. 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

  1. What signal does Kubernetes send to initiate shutdown? SIGTERM. The pod has terminationGracePeriodSeconds (default 30) to exit before SIGKILL.

  2. 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.

  3. 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.

  4. 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

Can I use SIGKILL to trigger graceful shutdown?

No. SIGKILL cannot be caught or handled. It immediately terminates the process. Always handle SIGTERM for graceful shutdown.

What happens if I don't register a SIGTERM handler?

The default SIGTERM behavior terminates the process immediately. Any pending cleanup is skipped.

Should I handle SIGUSR1 or SIGUSR2?

Handle them only if you have a specific use case. Node.js uses SIGUSR1 for the debugger by default.

How do I test signal handlers without sending real signals?

Call the handler function directly: process.emit('SIGTERM'). This runs the handler without sending a real OS signal.

Can I use worker threads with signal handlers?

Yes, but only the main thread receives signals. Worker threads must be notified of shutdown through a shared variable or message channel.

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