DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Graceful Shutdown in Node.js with Claude Code: Kubernetes-Ready Termination

When Kubernetes sends a SIGTERM during a rolling update, are your in-flight requests getting cut off? Claude Code can implement proper graceful shutdown — request draining, resource cleanup, and readiness probes — when you define the requirements.


CLAUDE.md for Graceful Shutdown

## Graceful Shutdown Policy

### Required implementation
When SIGTERM/SIGINT received:
1. Stop accepting new requests
2. Wait for in-flight requests to complete (max 30 seconds)
3. Disconnect external resources in order
4. Exit process

### Timeout
- Shutdown timeout: 30 seconds
- Force exit after timeout (exit code 1)

### Kubernetes requirements
- readinessProbe: GET /health/ready (return 503 during shutdown)
- terminationGracePeriodSeconds: 60 in Pod spec
- preStop hook: sleep 5 (wait for load balancer to route away)

### Cleanup order
1. HTTP server (stop accepting requests)
2. BullMQ workers (wait for in-progress jobs)
3. Prisma (disconnect DB)
4. Redis (quit connection)
Enter fullscreen mode Exit fullscreen mode

Generating the Shutdown Handler

Implement graceful shutdown for a Node.js + Express app.

Requirements:
- Triggered by SIGTERM and SIGINT
- Stop accepting new requests first
- Wait for in-flight requests to complete
- Timeout: 30 seconds (force exit on timeout)
- Cleanup: BullMQ worker  Prisma  Redis (in that order)
- Log each shutdown step

Save to: src/shutdown.ts
Enter fullscreen mode Exit fullscreen mode

Generated Shutdown Handler

// src/shutdown.ts
import http from 'http';
import { prisma } from './lib/prisma';
import { redis } from './lib/redis';
import { emailWorker } from './workers/emailWorker';
import { logger } from './lib/logger';

const SHUTDOWN_TIMEOUT_MS = 30_000;

export let isShuttingDown = false;

export function setupGracefulShutdown(server: http.Server): void {
  const shutdown = async (signal: string) => {
    if (isShuttingDown) return;
    isShuttingDown = true;

    logger.info({ signal }, 'Shutdown signal received, starting graceful shutdown');

    // Force exit after timeout
    const forceExit = setTimeout(() => {
      logger.error({ timeout: SHUTDOWN_TIMEOUT_MS }, 'Shutdown timeout exceeded, forcing exit');
      process.exit(1);
    }, SHUTDOWN_TIMEOUT_MS);

    try {
      // Step 1: Stop accepting new requests
      logger.info('Closing HTTP server (draining in-flight requests)...');
      await new Promise<void>((resolve, reject) => {
        server.close((err) => (err ? reject(err) : resolve()));
      });
      logger.info('HTTP server closed');

      // Step 2: Stop BullMQ worker (wait for current job to finish)
      logger.info('Closing BullMQ worker...');
      await emailWorker.close();
      logger.info('BullMQ worker closed');

      // Step 3: Disconnect Prisma
      logger.info('Disconnecting Prisma...');
      await prisma.$disconnect();
      logger.info('Prisma disconnected');

      // Step 4: Close Redis
      logger.info('Closing Redis connection...');
      await redis.quit();
      logger.info('Redis disconnected');

      clearTimeout(forceExit);
      logger.info('Graceful shutdown complete');
      process.exit(0);
    } catch (err) {
      logger.error({ err }, 'Error during graceful shutdown');
      clearTimeout(forceExit);
      process.exit(1);
    }
  };

  process.on('SIGTERM', () => shutdown('SIGTERM'));
  process.on('SIGINT', () => shutdown('SIGINT'));

  // Catch unhandled rejections before they crash the process silently
  process.on('unhandledRejection', (reason) => {
    logger.error({ reason }, 'Unhandled promise rejection');
  });
}
Enter fullscreen mode Exit fullscreen mode

Health Check Endpoints for Kubernetes

Generate health check endpoints for Kubernetes probes.

Endpoints:
- GET /health/live: Liveness probe (always OK if process is running)
- GET /health/ready: Readiness probe
  - Returns 503 if: DB unreachable, Redis unreachable, or shutting down
  - Returns 200 if all checks pass
  Response: { status: 'ok'|'error', checks: { db: bool, redis: bool } }

Save to: src/routes/health.ts
Enter fullscreen mode Exit fullscreen mode
import { Router } from 'express';
import { isShuttingDown } from '../shutdown';
import { prisma } from '../lib/prisma';
import { redis } from '../lib/redis';

export const healthRouter = Router();

// Liveness — is the process alive?
healthRouter.get('/health/live', (_req, res) => {
  res.json({ status: 'ok' });
});

// Readiness — can we serve requests?
healthRouter.get('/health/ready', async (_req, res) => {
  if (isShuttingDown) {
    return res.status(503).json({ status: 'error', reason: 'shutting_down' });
  }

  const checks = { db: false, redis: false };

  try {
    await prisma.$queryRaw`SELECT 1`;
    checks.db = true;
  } catch {}

  try {
    await redis.ping();
    checks.redis = true;
  } catch {}

  const allHealthy = Object.values(checks).every(Boolean);

  res.status(allHealthy ? 200 : 503).json({
    status: allHealthy ? 'ok' : 'error',
    checks,
  });
});
Enter fullscreen mode Exit fullscreen mode

Kubernetes Deployment Configuration

Generate Kubernetes Deployment spec with graceful shutdown support.

Requirements:
- terminationGracePeriodSeconds: 60
- preStop hook: sleep 5 (give load balancer time to route traffic away)
- readinessProbe: GET /health/ready every 3s, 2 failures = remove from pool
- livenessProbe: GET /health/live every 10s, 3 failures = restart pod
Enter fullscreen mode Exit fullscreen mode
spec:
  terminationGracePeriodSeconds: 60
  containers:
    - name: app
      lifecycle:
        preStop:
          exec:
            command: ["/bin/sleep", "5"]
      readinessProbe:
        httpGet:
          path: /health/ready
          port: 3000
        periodSeconds: 3
        failureThreshold: 2
      livenessProbe:
        httpGet:
          path: /health/live
          port: 3000
        periodSeconds: 10
        failureThreshold: 3
Enter fullscreen mode Exit fullscreen mode

Summary

Implement graceful shutdown with Claude Code:

  1. CLAUDE.md — Define shutdown order, timeout, Kubernetes requirements
  2. Shutdown handler — Stop HTTP → drain workers → disconnect resources
  3. Readiness probe — Kubernetes removes pod from load balancer during shutdown
  4. Kubernetes config — preStop + terminationGracePeriodSeconds for safe rollouts

Code Review Pack (¥980) includes /code-review for production readiness review — missing health checks, shutdown handlers, resource cleanup.

👉 prompt-works.jp

Myouga (@myougatheaxo) — Claude Code engineer focused on production operations.

Top comments (0)