bundled-skills/graceful-shutdown/SKILL.md
Implement graceful shutdown for servers and workers: drain connections, finish in-flight work, release resources, and exit cleanly on SIGTERM/SIGINT.
npx skillsauth add FrancoStino/opencode-skills-antigravity graceful-shutdownInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
A skill for implementing graceful shutdown in servers, workers, and long-running processes. Ensures in-flight requests complete, background jobs finish or checkpoint, database connections close cleanly, and the process exits with a proper status code. Essential for zero-downtime deployments in container orchestrators (Kubernetes, ECS, Docker Compose) and bare-metal process managers (systemd, PM2).
/healthz, /readyz) for orchestratorsTrap SIGTERM (orchestrator shutdown) and SIGINT (Ctrl+C) at process startup. Set a flag so the application knows it is shutting down.
let isShuttingDown = false;
function onShutdownSignal(signal: string): void {
if (isShuttingDown) return; // prevent double-shutdown
isShuttingDown = true;
console.log(`Received ${signal}, starting graceful shutdown...`);
shutdown();
}
process.on("SIGTERM", () => onShutdownSignal("SIGTERM"));
process.on("SIGINT", () => onShutdownSignal("SIGINT"));
Immediately stop the server from accepting new connections. For HTTP servers, call server.close(). For queue workers, stop polling for new jobs.
async function shutdown(): Promise<void> {
// 1. Stop accepting new connections
server.close(() => {
console.log("Server closed — no new connections accepted");
});
// 2. Mark health check as not-ready so load balancers stop routing
// (readiness probe returns 503 from this point)
}
Wait for active requests and background tasks to finish, but enforce a hard deadline so the process never hangs indefinitely.
const DRAIN_TIMEOUT_MS = 25_000; // must be less than orchestrator's terminationGracePeriodSeconds
async function drainAndExit(): Promise<void> {
const deadline = setTimeout(() => {
console.error("Drain timeout reached — forcing exit");
process.exit(1);
}, DRAIN_TIMEOUT_MS);
deadline.unref(); // don't keep the event loop alive just for the timer
try {
// Wait for active connections to finish
await waitForActiveConnections();
// Flush buffered data (logs, metrics, queues)
await flushBuffers();
// Close external resource handles
await closeResources();
console.log("Graceful shutdown complete");
process.exit(0);
} catch (err) {
console.error("Error during shutdown:", err);
process.exit(1);
}
}
Orchestrators use these to decide whether to route traffic and whether to restart the container.
import { createServer, IncomingMessage, ServerResponse } from "node:http";
function handleHealthCheck(req: IncomingMessage, res: ServerResponse): void {
if (req.url === "/healthz") {
// Liveness: is the process alive and not deadlocked?
res.writeHead(200).end("ok");
return;
}
if (req.url === "/readyz") {
// Readiness: should traffic be routed here?
if (isShuttingDown) {
res.writeHead(503).end("shutting down");
} else {
res.writeHead(200).end("ready");
}
return;
}
}
Maintain a count of in-flight requests so you know when draining is complete.
let activeConnections = 0;
let drainResolve: (() => void) | null = null;
function onRequestStart(): void {
activeConnections++;
}
function onRequestEnd(): void {
activeConnections--;
if (isShuttingDown && activeConnections === 0 && drainResolve) {
drainResolve();
}
}
function waitForActiveConnections(): Promise<void> {
if (activeConnections === 0) return Promise.resolve();
return new Promise((resolve) => {
drainResolve = resolve;
});
}
import express from "express";
import { createServer } from "node:http";
const app = express();
const server = createServer(app);
let isShuttingDown = false;
let activeRequests = 0;
// Track in-flight requests
app.use((req, res, next) => {
if (isShuttingDown) {
res.setHeader("Connection", "close");
res.status(503).json({ error: "Server is shutting down" });
return;
}
activeRequests++;
res.on("finish", () => activeRequests--);
next();
});
// Health endpoints
app.get("/healthz", (_, res) => res.send("ok"));
app.get("/readyz", (_, res) => {
res.status(isShuttingDown ? 503 : 200).send(isShuttingDown ? "draining" : "ready");
});
// Application routes
app.get("/api/data", async (req, res) => {
const data = await fetchData();
res.json(data);
});
// Graceful shutdown
function shutdown(signal: string): void {
if (isShuttingDown) return;
isShuttingDown = true;
console.log(`${signal} received — draining ${activeRequests} active requests`);
server.close();
const forceExit = setTimeout(() => {
console.error("Forced exit — drain timeout exceeded");
process.exit(1);
}, 25_000);
forceExit.unref();
const poll = setInterval(() => {
if (activeRequests === 0) {
clearInterval(poll);
console.log("All requests drained — exiting cleanly");
process.exit(0);
}
}, 100);
}
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));
server.listen(3000, () => console.log("Server ready on :3000"));
import asyncio
import signal
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, Response
active_requests = 0
is_shutting_down = False
shutdown_event = asyncio.Event()
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
loop = asyncio.get_event_loop()
loop.add_signal_handler(signal.SIGTERM, begin_shutdown)
yield
# Shutdown — wait for in-flight requests
if active_requests > 0:
try:
await asyncio.wait_for(shutdown_event.wait(), timeout=25.0)
except asyncio.TimeoutError:
print(f"Drain timeout — {active_requests} requests abandoned")
print("Shutdown complete")
app = FastAPI(lifespan=lifespan)
def begin_shutdown():
global is_shutting_down
is_shutting_down = True
print(f"SIGTERM received — draining {active_requests} requests")
if active_requests == 0:
shutdown_event.set()
@app.middleware("http")
async def track_requests(request: Request, call_next):
global active_requests
if is_shutting_down:
return Response("Service shutting down", status_code=503)
active_requests += 1
try:
response = await call_next(request)
return response
finally:
active_requests -= 1
if is_shutting_down and active_requests == 0:
shutdown_event.set()
@app.get("/healthz")
async def healthz():
return {"status": "ok"}
@app.get("/readyz")
async def readyz():
if is_shutting_down:
return Response("draining", status_code=503)
return {"status": "ready"}
import { parentPort } from "node:worker_threads";
let isShuttingDown = false;
let currentJob: { id: string; checkpoint: () => Promise<void> } | null = null;
process.on("SIGTERM", async () => {
isShuttingDown = true;
console.log("Worker shutting down — finishing current job");
if (currentJob) {
await currentJob.checkpoint();
console.log(`Job ${currentJob.id} checkpointed`);
}
process.exit(0);
});
async function processJobs(queue: JobQueue): Promise<void> {
while (!isShuttingDown) {
const job = await queue.poll({ timeout: 5000 });
if (!job) continue;
currentJob = job;
await job.execute();
await queue.ack(job.id);
currentJob = null;
}
}
terminationGracePeriodSeconds in Kubernetes defaults to 30s — use 25s for your drain)Connection: close header on responses sent during draining so HTTP/1.1 clients don't reuse the connectionprocess.exit(0) for clean shutdown and process.exit(1) for timeout/error so orchestrators can distinguish the twoProblem: Kubernetes kills the pod before connections drain because terminationGracePeriodSeconds is too short.
Solution: Set it to at least drain timeout + 5s buffer. If your longest request takes 60s, use terminationGracePeriodSeconds: 70 and drain timeout of 65s.
Problem: Load balancer keeps sending traffic after SIGTERM because readiness probe still returns 200. Solution: Flip the readiness probe to 503 immediately on signal receipt — before starting to drain.
Problem: server.close() resolves instantly but connections remain open (keep-alive).
Solution: Track connections manually and destroy idle keep-alive sockets on shutdown. Active sockets with in-flight requests should drain normally.
Problem: Double shutdown from both SIGTERM and SIGINT (e.g., Docker sends SIGTERM then user hits Ctrl+C).
Solution: Guard with a isShuttingDown flag — ignore the second signal.
Problem: Deadlocked process never exits because drain waits forever. Solution: Always have a hard force-exit timeout as the final backstop.
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
terminationGracePeriodSeconds: 30
containers:
- name: app
livenessProbe:
httpGet:
path: /healthz
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /readyz
port: 3000
initialDelaySeconds: 2
periodSeconds: 5
server.close() alone won't gracefully end them.cluster module), each worker must handle signals independently.@api-rate-limit-handler — Resilient retry and backoff for outbound requests@circuit-breaker — When to stop retrying entirely and fail fast@error-handling — Structured error handling patternstools
Authorized security assessment of LLM applications and AI agents: prompt injection, tool abuse, RAG exposure, memory poisoning, system-prompt extraction, and agent-compliance engineering per OWASP LLM/ASI Top 10.
development
Builds two parameterized UI modes—流光溢彩白 (iridescent white) and 五彩斑斓黑 (colorful black)—with OKLCH, WebGL/CSS fallback, vision gating, screenshot QA, and total/per-color intensity reports. Use when a UI request names either mode or needs measured color parameters.
tools
Delegate coding tasks to the Kimi Code CLI (`kimi`) only when the user explicitly requests it, while the orchestrator retains review and landing responsibility.
development
Front-end JavaScript reverse engineering: locate signature chains, analyze encrypted request parameters, sample runtime behavior, and reproduce logic locally in Node for evidence-based output.