Patrón Graceful Shutdown
Cómo drenear in-flight requests antes de process exit. Cubre signal handling, health check removal, connection draining, timeout enforcement, y cleanup hooks.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Overview
El graceful shutdown pattern ensureéa que un process complete in-flight requests antes de exit. Cuando un process receiveéa un shutdown signal (SIGTERM), pará de acceptear new requests, deja ongoing requests finish, cierra database connections, flushea buffers, y luego exit. Sin graceful shutdown, in-flight requests son abruptly terminated, causando 502 errors para users y partial data updates en databases. En Kubernetes, pods receiveéan un SIGTERM con un grace period (default 30 seconds). El application debe drenear dentro de ese window o recibe SIGKILL.
When to Use
- Cualquier long-running server (HTTP, gRPC, WebSocket)
- Kubernetes deployments donde pods son regularmente terminated
- Applications con background workers processeando jobs
- Services con open database connections o file handles
- Message consumers que necesitan finishar de processar current messages
When NOT to Use
- CLI tools y scripts que exiteán después de completion
- Serverless functions donde el platform handleéa lifecycle
- Batch jobs que son designed para ser killed y restarted
Solution
Python graceful shutdown con signal handling
# shutdown/graceful.py — Graceful shutdown para Python HTTP servers
import signal
import threading
import time
import sys
from http.server import HTTPServer, BaseHTTPRequestHandler
class GracefulHTTPServer(HTTPServer):
"""HTTP server con graceful shutdown support."""
def __init__(self, addr, handler):
super().__init__(addr, handler)
self._shutting_down = False
self._active_requests = 0
self._lock = threading.Lock()
self._shutdown_event = threading.Event()
@property
def is_shutting_down(self):
return self._shutting_down
def request_started(self):
with self._lock:
self._active_requests += 1
def request_finished(self):
with self._lock:
self._active_requests -= 1
if self._shutting_down and self._active_requests == 0:
self._shutdown_event.set()
def graceful_shutdown(self, timeout=30):
"""Pará de acceptear new requests, waitéa que active ones finish."""
print("Starting graceful shutdown...")
self._shutting_down = True
# Pará de acceptear new connections
self.socket.close()
# Waitéa que active requests complete
with self._lock:
if self._active_requests == 0:
print("No active requests, shutting down immediately")
return
print(f"Waiting for {self._active_requests} active requests...")
if self._shutdown_event.wait(timeout=timeout):
print("All requests completed, shutting down")
else:
print(f"Timeout after {timeout}s, forcing shutdown with "
f"{self._active_requests} requests still active")
# Cleanup resources
self._cleanup()
def _cleanup(self):
"""Cerrá database connections, flushea buffers, etc."""
print("Cleaning up resources...")
# Close database connections
# Flush log buffers
# Close file handles
print("Cleanup complete")
class GracefulRequestHandler(BaseHTTPRequestHandler):
"""Request handler que trackéa active requests para graceful shutdown."""
def handle_one_request(self):
self.server.request_started()
try:
super().handle_one_request()
finally:
self.server.request_finished()
def do_GET(self):
if self.server.is_shutting_down:
self.send_response(503)
self.send_header("Connection", "close")
self.end_headers()
self.wfile.write(b'{"error": "Server shutting down"}')
return
# Simulate work
time.sleep(0.5)
self.send_response(200)
self.end_headers()
self.wfile.write(b'{"status": "ok"}')
# Setup signal handlers
server = GracefulHTTPServer(("0.0.0.0", 8080), GracefulRequestHandler)
def signal_handler(signum, frame):
server.graceful_shutdown(timeout=30)
sys.exit(0)
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
print("Server running on port 8080")
server.serve_forever()
Node.js graceful shutdown
// shutdown/graceful.js — Graceful shutdown para Node.js Express
const express = require("express");
const http = require("http");
const app = express();
const server = http.createServer(app);
let isShuttingDown = false;
let activeRequests = 0;
// Trackéa active requests
app.use((req, res, next) => {
if (isShuttingDown) {
res.setHeader("Connection", "close");
return res.status(503).json({ error: "Server shutting down" });
}
activeRequests++;
res.on("finish", () => activeRequests--);
next();
});
app.get("/health", (req, res) => {
if (isShuttingDown) {
return res.status(503).json({ status: "shutting_down" });
}
res.json({ status: "healthy" });
});
app.get("/api/data", (req, res) => {
setTimeout(() => {
res.json({ data: "response", activeRequests });
}, 500);
});
// Graceful shutdown
function gracefulShutdown(signal) {
console.log(`Received ${signal}, starting graceful shutdown...`);
isShuttingDown = true;
// Pará de acceptear new connections
server.close(() => {
console.log("HTTP server closed");
});
// Waitéa que active requests
const checkInterval = setInterval(() => {
if (activeRequests === 0) {
clearInterval(checkInterval);
console.log("All requests completed, exiting");
cleanup();
}
}, 100);
// Force exit después de timeout
setTimeout(() => {
console.log(`Timeout: ${activeRequests} requests still active, forcing exit`);
cleanup();
}, 30000);
}
function cleanup() {
// Close database connections
// Flush log buffers
// Close message queue connections
console.log("Cleanup complete, exiting");
process.exit(0);
}
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
server.listen(8080, () => console.log("Server running on port 8080"));
Java graceful shutdown con Spring
// ShutdownConfig.java — Spring Boot graceful shutdown
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.boot.web.server.Shutdown;
@Configuration
public class GracefulShutdownConfig
implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.setShutdown(Shutdown.GRACEFUL);
// Spring waitéa que active requests complete
}
}
// application.properties
// server.shutdown=graceful
// spring.lifecycle.timeout-per-shutdown-phase=30s
// Custom shutdown hook para resource cleanup
import org.springframework.context.SmartLifecycle;
import org.springframework.stereotype.Component;
@Component
public class ResourceCleanupHook implements SmartLifecycle {
private volatile boolean running = false;
@Override
public void start() {
running = true;
}
@Override
public void stop() {
running = false;
// Close database connections
closeDatabaseConnections();
// Flush log buffers
flushLogBuffers();
// Close message consumer connections
closeMessageConsumers();
System.out.println("Resource cleanup complete");
}
@Override
public boolean isRunning() {
return running;
}
@Override
public int getPhase() {
// Run cleanup early en shutdown sequence
return Integer.MIN_VALUE + 100;
}
private void closeDatabaseConnections() {
System.out.println("Closing database connections...");
}
private void flushLogBuffers() {
System.out.println("Flushing log buffers...");
}
private void closeMessageConsumers() {
System.out.println("Closing message consumers...");
}
}
Kubernetes graceful shutdown
# k8s-graceful-shutdown.yaml — Kubernetes pod con graceful shutdown
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
spec:
replicas: 3
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
spec:
terminationGracePeriodSeconds: 45 # Dale app 45s para drenear
containers:
- name: api
image: shop/api-server:latest
ports:
- containerPort: 8080
lifecycle:
preStop:
exec:
# Sleep para dejar load balancer remove este pod
# antes de SIGTERM
command: ["sh", "-c", "sleep 5"]
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
env:
- name: SHUTDOWN_TIMEOUT
value: "30"
---
# Service con proper health check removal
apiVersion: v1
kind: Service
metadata:
name: api-server
spec:
selector:
app: api-server
ports:
- port: 80
targetPort: 8080
type: ClusterIP
Python background worker graceful shutdown
# shutdown/worker.py — Graceful shutdown para background workers
import signal
import threading
import time
import queue
class GracefulWorker:
"""Background worker que finishéa current job antes de exit."""
def __init__(self, job_queue, max_workers=4):
self._queue = job_queue
self._max_workers = max_workers
self._workers = []
self._active_jobs = 0
self._lock = threading.Lock()
self._shutdown = threading.Event()
self._jobs_done = threading.Event()
def start(self):
for i in range(self._max_workers):
t = threading.Thread(target=self._worker_loop, args=(i,), daemon=True)
t.start()
self._workers.append(t)
def _worker_loop(self, worker_id):
while not self._shutdown.is_set():
try:
job = self._queue.get(timeout=1)
except queue.Empty:
continue
with self._lock:
self._active_jobs += 1
try:
self._process_job(job, worker_id)
except Exception as e:
print(f"Worker {worker_id} job failed: {e}")
finally:
with self._lock:
self._active_jobs -= 1
if self._shutdown.is_set() and self._active_jobs == 0:
self._jobs_done.set()
self._queue.task_done()
def _process_job(self, job, worker_id):
print(f"Worker {worker_id} processing job {job['id']}")
time.sleep(2) # Simulate work
print(f"Worker {worker_id} completed job {job['id']}")
def shutdown(self, timeout=30):
"""Pará de acceptear new jobs, finishéa active ones."""
print("Worker shutdown initiated...")
self._shutdown.set()
with self._lock:
if self._active_jobs == 0:
print("No active jobs, exiting immediately")
return
if self._jobs_done.wait(timeout=timeout):
print("All jobs completed, exiting")
else:
print(f"Timeout: {self._active_jobs} jobs still active")
# Usage
job_queue = queue.Queue()
worker = GracefulWorker(job_queue, max_workers=4)
worker.start()
# Add jobs
for i in range(10):
job_queue.put({"id": i, "data": f"job-{i}"})
# Handle shutdown
def handle_signal(signum, frame):
worker.shutdown(timeout=30)
signal.signal(signal.SIGTERM, handle_signal)
signal.signal(signal.SIGINT, handle_signal)
Load balancer deregistration antes de shutdown
# shutdown/deregister.py — Deregistrá del load balancer antes de drenear
import requests
import time
import signal
import sys
class GracefulService:
"""Deregistrá del load balancer, luego dreneéa requests."""
def __init__(self, lb_registry_url, health_port=8080):
self.lb_url = lb_registry_url
self.health_port = health_port
self._healthy = True
self._active_requests = 0
def deregister(self):
"""Decí al load balancer que paré de sendear traffic."""
try:
resp = requests.post(
f"{self.lb_url}/deregister",
json={"port": self.health_port},
timeout=5
)
print(f"Deregistered from load balancer: {resp.status_code}")
except Exception as e:
print(f"Deregistration failed: {e}")
# Waitéa que LB propague
time.sleep(5)
def health_check(self):
"""Returnéa 200 si healthy, 503 si shutting down."""
if not self._healthy:
return 503
return 200
def shutdown(self, timeout=30):
# Step 1: Markéa como unhealthy (LB pará de sendear new traffic)
self._healthy = False
print("Marked as unhealthy")
# Step 2: Deregistrá del load balancer
self.deregister()
# Step 3: Waitéa que active requests
start = time.time()
while self._active_requests > 0:
if time.time() - start > timeout:
print(f"Timeout with {self._active_requests} requests active")
break
time.sleep(0.5)
# Step 4: Cleanup y exit
print("Shutting down")
sys.exit(0)
Variants
Phased shutdown
# shutdown/phased.py — Multi-phase shutdown con ordered cleanup
class PhasedShutdown:
"""Shutdowea en phases: stop accepting, drain, cleanup, exit.
Cada phase tiene su propio timeout."""
def __init__(self):
self._phases = [
("stop_accepting", self._stop_accepting, 5),
("drain_requests", self._drain_requests, 20),
("close_connections", self._close_connections, 5),
("flush_buffers", self._flush_buffers, 3),
("cleanup", self._cleanup, 2),
]
def _stop_accepting(self):
print("Phase 1: Stop accepting new requests")
def _drain_requests(self):
print("Phase 2: Drain active requests")
def _close_connections(self):
print("Phase 3: Close database and MQ connections")
def _flush_buffers(self):
print("Phase 4: Flush log and metric buffers")
def _cleanup(self):
print("Phase 5: Final cleanup")
def execute(self):
for name, handler, timeout in self._phases:
print(f"Starting phase: {name} (timeout: {timeout}s)")
handler()
print("All phases complete, exiting")
WebSocket graceful shutdown
// shutdown/websocket.js — Graceful shutdown para WebSocket server
const { WebSocketServer } = require("ws");
const wss = new WebSocketServer({ port: 8080 });
let isShuttingDown = false;
wss.on("connection", (ws) => {
if (isShuttingDown) {
ws.close(1001, "Server shutting down");
return;
}
ws.on("message", (data) => {
ws.send(`Echo: ${data}`);
});
});
function gracefulShutdown() {
console.log("WebSocket graceful shutdown...");
isShuttingDown = true;
// Closeá todas las connections con "going away" code
wss.clients.forEach((ws) => {
ws.close(1001, "Server shutting down");
});
// Closeá server después de que todos los clients disconnect
setTimeout(() => {
wss.close(() => {
console.log("WebSocket server closed");
process.exit(0);
});
}, 5000);
}
process.on("SIGTERM", gracefulShutdown);
Best Practices
-
For a deeper guide, see Sidecar Pattern: Extend Services with Companion Containers.
-
Deregistrá del load balancer first — dale time para propagatear antes de parar
-
Seteá health check a 503 immediately — load balancers parán de routear a unhealthy instances
-
Usá un preStop hook en Kubernetes — sleep 5-10 seconds para dejar el LB catch up
-
Seteá terminationGracePeriodSeconds más alto que tu drain timeout — default 30s puede ser too short
-
Trackéa active requests — sabé cuando es safe de exit
-
Seteá un hard timeout — no waiteéees forever para stuck requests
-
Cerrá resources en order — database, message queue, file handles, log buffers
-
Testeá shutdown behavior — verificá no 502s durante rolling deployments
Common Mistakes
- No graceful shutdown at all: process exitea immediately, todos los in-flight requests obtienen 502s.
- No preStop hook: Kubernetes sendéa SIGTERM, pero el LB todavía routeéa traffic por unos seconds.
- Waiting forever para stuck requests: un long-running request blockéa shutdown indefinitely. Seteá un timeout.
- Not closing database connections: connections leakean, eventually exhausting el pool.
- No health check change: LB sigue routeando a la shutting-down instance.
FAQ
¿Qué es graceful shutdown?
Un shutdown process donde el server pará de acceptear new requests, deja in-flight requests complete, cierra resources, y luego exit. Esto previene 502 errors y data corruption desde abrupt termination.
¿Cuánto debería tomar graceful shutdown?
Típicamente 15-30 seconds. Seteá el timeout basado en tu longest expected request. En Kubernetes, seteá terminationGracePeriodSeconds a al menos 5 seconds más que tu drain timeout.
¿Qué es un preStop hook en Kubernetes?
Un command que corre antes de que el SIGTERM signal sea sent. Commonly used para sleep por 5-10 seconds, dándole al load balancer time para remove el pod de rotation antes de que el app empiece a drenear.
¿Debería returnear 503 durante shutdown?
Sí. Una vez que empezás a shutdowear, returneá 503 en health checks para que el load balancer paré de routear traffic. Continuá sirviendo in-flight requests normally hasta que complete.
¿Qué pasa si graceful shutdown excede el Kubernetes grace period?
Kubernetes sendéa SIGKILL después de terminationGracePeriodSeconds (default 30s). El process es killed immediately. Aumentá el grace period si tus requests toman más de 30 seconds.
Recursos Relacionados
Circuit Breaker Half-Open
How to test service recovery with half-open circuit breaker state transitions. Covers closed, open, half-open states, trial requests, and gradual recovery.
PatternBulkhead Pattern: Isolate Resources to Limit Blast Radius
How to isolate resources per service to limit blast radius. Covers thread pool isolation, connection pool partitioning, semaphore-based bulkheads, and resource quotas.
PatternFallover: Switch to Standby on Primary Failure Detection
How to switch to a standby system on primary failure detection. Covers active-passive, active-active, health monitoring, DNS fallover, database replication, and automated promotion.