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)
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
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');
});
}
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
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,
});
});
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
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
Summary
Implement graceful shutdown with Claude Code:
- CLAUDE.md — Define shutdown order, timeout, Kubernetes requirements
- Shutdown handler — Stop HTTP → drain workers → disconnect resources
- Readiness probe — Kubernetes removes pod from load balancer during shutdown
- 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.
Myouga (@myougatheaxo) — Claude Code engineer focused on production operations.
Top comments (0)