Skip to content

Kubernetes Termination — Complete Implementation Guide

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Kubernetes Termination. We cover key concepts, practical examples, and best practices to help you master this topic.

Kubernetes pod termination follows a specific lifecycle: preStop hook runs, SIGTERM is sent, the pod is removed from service endpoints, and after the grace period, SIGKILL terminates remaining processes.

What You'll Learn

By the end of this tutorial, you will understand the complete Kubernetes pod termination sequence, how to configure preStop hooks, set appropriate termination grace periods, and ensure zero-downtime deployments.

Why It Matters

Kubernetes is the most common deployment platform. Understanding its termination lifecycle is essential for implementing graceful shutdown correctly in containerized environments.

Real-World Use

DodaTech's Kubernetes deployments use a 40-second terminationGracePeriodSeconds with a preStop hook that calls the application's shutdown-prepare endpoint and waits 5 seconds for endpoint propagation.

Kubernetes Termination Learning Path

flowchart LR
  A[Health Check During Shutdown] --> B[Kubernetes Termination]
  B --> C[preStop Hooks]
  B --> D[Grace Period]
  B --> E[SIGKILL Safety]
  B --> F{You Are Here}
  style F fill:#f90,color:#fff

The Kubernetes Termination Sequence

The termination sequence has six distinct phases with specific timing.

class KubernetesTerminationSequence {
  constructor() {
    this.phases = [
      { name: "1. Pod status changes to Terminating", duration: 0 },
      { name: "2. preStop hook executes", duration: "configurable" },
      { name: "3. SIGTERM sent to PID 1", duration: 0 },
      { name: "4. Endpoint removed from Service", duration: "1-5s" },
      { name: "5. Application drains connections", duration: "remaining grace" },
      { name: "6. SIGKILL after grace period", duration: "after timeout" }
    ];
  }

  explain() {
    this.phases.forEach(p => console.log(`${p.name} (${p.duration})`));
  }

  calculateTiming(preStopMs, appShutdownMs, gracePeriodMs) {
    const total = preStopMs + appShutdownMs;
    const buffer = gracePeriodMs - total;
    return {
      preStopMs,
      appShutdownMs,
      gracePeriodMs,
      totalNeeded: total,
      bufferMs: buffer,
      safe: buffer >= 5000
    };
  }
}

const k8s = new KubernetesTerminationSequence();
k8s.explain();
const timing = k8s.calculateTiming(5000, 25000, 40000);
console.log("Timing:", timing);
// 1. Pod status changes to Terminating (0)
// 2. preStop hook executes (configurable)
// 3. SIGTERM sent to PID 1 (0)
// 4. Endpoint removed from Service (1-5s)
// 5. Application drains connections (remaining grace)
// 6. SIGKILL after grace period (after timeout)
// Timing: { preStopMs: 5000, appShutdownMs: 25000, gracePeriodMs: 40000, totalNeeded: 30000, bufferMs: 10000, safe: true }

preStop Hook Configuration

The preStop hook runs before SIGTERM, giving the application a chance to prepare for shutdown.

class PreStopHook {
  constructor() {
    this.hooks = [];
  }

  addCommand(name, command) {
    this.hooks.push({ name, command });
  }

  async execute() {
    console.log(`Executing ${this.hooks.length} preStop hooks`);
    for (const { name, command } of this.hooks) {
      console.log(`  Hook: ${name}`);
      console.log(`  Command: ${command}`);
      await this.runCommand(command);
      console.log(`  Completed: ${name}`);
    }
  }

  async runCommand(command) {
    // Simulate running a command
    await new Promise(r => setTimeout(r, 500));
  }

  generateYaml() {
    const commands = this.hooks.map(h => ({
      exec: { command: ["/bin/sh", "-c", h.command] }
    }));
    return {
      lifecycle: {
        preStop: commands.length === 1 ? commands[0] : { exec: { command: ["/bin/sh", "-c", commands.map(c => c.exec.command.slice(-1)[0]).join(" && ")] } }
      }
    };
  }
}

const preStop = new PreStopHook();
preStop.addCommand("mark-unhealthy", "curl -X POST http://localhost:8080/shutdown-prepare");
preStop.addCommand("wait-drain", "sleep 5");
console.log("PreStop YAML:", JSON.stringify(preStop.generateYaml(), null, 2));
// PreStop YAML: { "lifecycle": { "preStop": { "exec": { "command": [ ... ] } } } }

Configuring Termination Grace Period

The terminationGracePeriodSeconds in the pod spec controls how long Kubernetes waits before SIGKILL.

class GracePeriodCalculator {
  static suggest(maxRequestDurationMs, drainTimeMs, safetyBufferMs = 5000) {
    const suggested = maxRequestDurationMs + drainTimeMs + safetyBufferMs;
    return {
      maxRequestDurationMs,
      drainTimeMs,
      safetyBufferMs,
      suggestedGracePeriodMs: suggested,
      suggestedGracePeriodSec: Math.ceil(suggested / 1000),
      yamlValue: `terminationGracePeriodSeconds: ${Math.ceil(suggested / 1000)}`
    };
  }

  static validate(currentGraceSec, maxRequestSec, drainSec) {
    const needed = maxRequestSec + drainSec + 5;
    return {
      current: currentGraceSec,
      needed: needed,
      adequate: currentGraceSec >= needed,
      shortfall: Math.max(0, needed - currentGraceSec),
      recommendation: currentGraceSec >= needed
        ? "Grace period is adequate"
        : `Increase by at least ${needed - currentGraceSec} seconds`
    };
  }
}

console.log(GracePeriodCalculator.suggest(30000, 5000, 5000));
console.log(GracePeriodCalculator.validate(30, 30, 5));
// { suggestedGracePeriodMs: 40000, suggestedGracePeriodSec: 40, ... }
// { current: 30, needed: 40, adequate: false, shortfall: 10, ... }

Handling PID 1 and Signal Forwarding

In containers, the main Process runs as PID 1 and must handle signals. Some runtimes require special handling.

class PidOneHandler {
  static explain(challenges, solutions) {
    console.log("PID 1 Challenges:");
    challenges.forEach(c => console.log(`  - ${c}`));
    console.log("Solutions:");
    solutions.forEach(s => console.log(`  - ${s}`));
  }

  static usingTini() {
    return {
      approach: "Use tini or dumb-init as entrypoint",
      dockerfile: `ENTRYPOINT ["/usr/bin/tini", "--"]\nCMD ["node", "app.js"]`,
      benefit: "Tini handles signal forwarding and reaping zombie processes"
    };
  }

  static usingExecForm() {
    return {
      approach: "Use exec form in CMD/ENTRYPOINT",
      dockerfile: `CMD ["node", "app.js"]`,
      benefit: "Exec form makes the app PID 1, shell form does not forward signals"
    };
  }
}

PidOneHandler.explain(
  ["Shell form CMD (node app.js) doesn't forward signals", "Zombie processes accumulate without a reaper"],
  ["Use exec form CMD [\"node\", \"app.js\"]", "Use tini as entrypoint", "Use --init flag in Docker"]
);

Pod Disruption Budgets

PodDisruptionBudgets ensure a minimum number of pods remain available during voluntary disruptions like deployments.

class PodDisruptionBudgetHelper {
  static create(minAvailable, maxUnavailable, selector) {
    return {
      apiVersion: "policy/v1",
      kind: "PodDisruptionBudget",
      metadata: { name: "app-pdb" },
      spec: {
        minAvailable: minAvailable || undefined,
        maxUnavailable: maxUnavailable || undefined,
        selector: { matchLabels: selector }
      }
    };
  }

  static explain() {
    console.log("PDB ensures N pods always stay up during rolling updates");
    console.log("Two modes: minAvailable (minimum absolute)");
    console.log("           maxUnavailable (percentage or absolute)");
    console.log("Example: minAvailable: 2 keeps at least 2 pods running");
  }
}

console.log(PodDisruptionBudgetHelper.create(2, undefined, { app: "my-service" }));
// { apiVersion: "policy/v1", kind: "PodDisruptionBudget", ... }

Common Mistakes

  1. Setting terminationGracePeriodSeconds too low -- If the application can't shut down within the grace period, Kubernetes sends SIGKILL and all graceful shutdown work is wasted.

  2. Not using preStop hooks -- Without preStop, the readiness probe remains healthy until the SIGTERM handler runs, causing traffic to arrive during shutdown.

  3. Putting sleep in preStop instead of application drain -- The preStop should trigger the application's shutdown preparation, not just wait. Sleep is only for endpoint propagation delay.

  4. Forgetting that SIGTERM goes to PID 1 -- Using shell form CMD (node app.js) runs a shell as PID 1, which doesn't forward SIGTERM to the Node.js process.

  5. Not considering eviction API -- Node-pressure eviction sends SIGTERM with a shorter grace period. Ensure the application can shut down within the shorter eviction timeout.

Practice Questions

  1. What is the order of the Kubernetes pod termination sequence? Pod state changes to Terminating -> preStop hook runs -> SIGTERM sent -> endpoint removed -> application drains -> SIGKILL after grace period.

  2. How do you configure the maximum time Kubernetes waits before SIGKILL? Set terminationGracePeriodSeconds in the pod spec. Default is 30 seconds.

  3. What is the purpose of the preStop hook? To execute commands before SIGTERM is sent, typically used to mark the pod as unhealthy and wait for endpoint propagation.

  4. Challenge: Write a script that simulates the Kubernetes termination sequence with configurable timing.

class K8sTerminationSimulator {
  constructor(gracePeriodSec = 30) {
    this.gracePeriodMs = gracePeriodSec * 1000;
    this.startTime = null;
  }

  async simulate() {
    this.startTime = Date.now();
    console.log("Pod state: Running -> Terminating");

    console.log("Phase 1: preStop hook (sleep 5s)");
    await this.sleep(5000);

    console.log("Phase 2: SIGTERM sent");
    console.log("Phase 3: Endpoint removal (1-5s)");

    const drainStart = Date.now();
    const remaining = this.gracePeriodMs - (drainStart - this.startTime);
    console.log(`Phase 4: Draining (${remaining}ms remaining)`);

    const drainTime = Math.min(remaining, 20000);
    await this.sleep(drainTime);

    const elapsed = Date.now() - this.startTime;
    if (elapsed >= this.gracePeriodMs) {
      console.log("Phase 5: SIGKILL - process terminated");
    } else {
      console.log("Phase 5: Process exited cleanly");
    }
  }

  sleep(ms) {
    return new Promise(r => setTimeout(r, ms));
  }
}

const sim = new K8sTerminationSimulator(30);
sim.simulate();

FAQ

What happens if the preStop hook fails?

Kubernetes still sends SIGTERM after the hook completes or times out. The pod is not retried or rescheduled.

Can the preStop hook be an HTTP request instead of a command?

Not directly. Use curl or wget in a shell command to make HTTP requests to the application.

What is the default terminationGracePeriodSeconds?

30 seconds. This applies to both voluntary (deployment updates) and involuntary (node pressure) evictions.

How do I handle shutdown in a multi-container pod?

Each container in the pod terminates independently. Sidecar containers should have their own preStop hooks and shutdown logic.

What happens during node-pressure eviction?

The kubelet sends SIGTERM with a shorter grace period (node-pressure-eviction). Ensure the application can shut down within the reduced time.

Mini Project

Build a Kubernetes-aware graceful shutdown handler that implements the complete termination sequence: preStop preparation, SIGTERM handling, endpoint drain delay, and configurable grace period timing.

class K8sGracefulShutdown {
  constructor(options = {}) {
    this.gracePeriodMs = options.gracePeriodMs || 30000;
    this.endpointDrainDelay = options.endpointDrainDelay || 5000;
    this.state = "running";
    this.steps = [];
  }

  addStep(name, fn) {
    this.steps.push({ name, fn });
  }

  async shutdown() {
    console.log("Kubernetes termination sequence started");

    console.log("1. preStop phase: mark endpoint unhealthy");
    this.state = "prestop";
    await this.sleep(this.endpointDrainDelay);

    console.log("2. SIGTERM received: starting drain");
    this.state = "draining";

    const startTime = Date.now();
    for (const { name, fn } of this.steps) {
      const remaining = this.gracePeriodMs - (Date.now() - startTime);
      if (remaining <= 0) break;
      await Promise.race([
        fn(),
        this.sleep(remaining)
      ]);
      console.log(`  Step '${name}' complete`);
    }

    const elapsed = Date.now() - startTime;
    if (elapsed < this.gracePeriodMs) {
      console.log("3. Clean exit within grace period");
      process.exit(0);
    } else {
      console.log("3. SIGKILL would terminate now");
    }
  }

  sleep(ms) {
    return new Promise(r => setTimeout(r, ms));
  }
}

const k8sShutdown = new K8sGracefulShutdown({ gracePeriodMs: 25000 });
k8sShutdown.addStep("drain-http", () => Promise.resolve());
k8sShutdown.addStep("close-db", () => Promise.resolve());
k8sShutdown.addStep("close-queue", () => Promise.resolve());

What's Next

Now that you understand Kubernetes termination, learn about zero-downtime deployment strategies. Then explore implementing graceful shutdown in Express.js.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro