SIGTERM handling by language
2 min read
Pod lifecycle overview · Checklist and references
SIGTERM handling patterns by language
Node.js (Express)
const express = require('express');
const app = express();
const server = app.listen(8080);
// State flag
let isShuttingDown = false;
// Health check endpoints
app.get('/healthz', (req, res) => {
res.status(200).json({ status: 'ok' });
});
app.get('/ready', (req, res) => {
if (isShuttingDown) {
return res.status(503).json({ status: 'shutting_down' });
}
res.status(200).json({ status: 'ready' });
});
// Business logic
app.get('/api/data', (req, res) => {
if (isShuttingDown) {
return res.status(503).send('Service Unavailable');
}
// Actual logic
res.json({ data: 'example' });
});
// Handle graceful shutdown
function gracefulShutdown(signal) {
console.log(`${signal} received, starting graceful shutdown`);
isShuttingDown = true;
// Reject new connections
server.close(() => {
console.log('HTTP server closed');
// Close database connections
// db.close();
// Exit the process
process.exit(0);
});
// Set a timeout (finish before SIGKILL)
setTimeout(() => {
console.error('Graceful shutdown timeout, forcing exit');
process.exit(1);
}, 50000); // terminationGracePeriodSeconds - preStop duration - 5-second buffer
}
// Handle SIGTERM and SIGINT
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
console.log('Server started on port 8080');
Deployment configuration:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nodejs-app
spec:
replicas: 3
selector:
matchLabels:
app: nodejs-app
template:
metadata:
labels:
app: nodejs-app
spec:
containers:
- name: app
image: myapp/nodejs:v1
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"]
terminationGracePeriodSeconds: 60
Java/Spring Boot
Spring Boot 2.3+ supports graceful shutdown natively.
application.yml:
server:
shutdown: graceful # Enable graceful shutdown
spring:
lifecycle:
timeout-per-shutdown-phase: 50s # Maximum wait time per shutdown phase (not the total shutdown time; does not bound a synchronous ContextClosedEvent listener)
management:
endpoints:
web:
exposure:
include: health
endpoint:
health:
probes:
enabled: true
health:
livenessState:
enabled: true
readinessState:
enabled: true
Custom shutdown logic (if needed):
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class GracefulShutdownListener {
@EventListener
public void onApplicationEvent(ContextClosedEvent event) {
System.out.println("Graceful shutdown initiated");
// Custom cleanup tasks
// For example: clean up message queues and wait for batch jobs to finish
try {
// Wait up to 50 seconds
cleanupResources();
} catch (Exception e) {
System.err.println("Cleanup error: " + e.getMessage());
}
}
private void cleanupResources() throws InterruptedException {
// Resource cleanup logic
Thread.sleep(5000); // Example: 5-second cleanup task
System.out.println("Cleanup completed");
}
}
Deployment configuration:
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-boot-app
spec:
replicas: 3
selector:
matchLabels:
app: spring-boot-app
template:
metadata:
labels:
app: spring-boot-app
spec:
containers:
- name: app
image: myapp/spring-boot:v2.7
ports:
- containerPort: 8080
env:
- name: JAVA_OPTS
value: "-Xms1g -Xmx2g"
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
periodSeconds: 5
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"]
terminationGracePeriodSeconds: 60
Go
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"sync/atomic"
"syscall"
"time"
)
// Read by concurrent HTTP handler goroutines and written by main, so synchronize with atomic
var isShuttingDown atomic.Bool
func main() {
// Configure the HTTP server
mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "ok")
})
mux.HandleFunc("/ready", func(w http.ResponseWriter, r *http.Request) {
if isShuttingDown.Load() {
w.WriteHeader(http.StatusServiceUnavailable)
fmt.Fprintln(w, "shutting down")
return
}
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "ready")
})
mux.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
if isShuttingDown.Load() {
w.WriteHeader(http.StatusServiceUnavailable)
return
}
// Business logic
fmt.Fprintln(w, `{"data":"example"}`)
})
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
// Start the server in a separate goroutine
go func() {
log.Println("Server starting on :8080")
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server error: %v", err)
}
}()
// Wait for SIGTERM/SIGINT
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
<-quit
log.Println("Graceful shutdown initiated")
isShuttingDown.Store(true)
// Graceful shutdown with timeout
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("Server exited gracefully")
}
Deployment configuration:
apiVersion: apps/v1
kind: Deployment
metadata:
name: go-app
spec:
replicas: 3
selector:
matchLabels:
app: go-app
template:
metadata:
labels:
app: go-app
spec:
containers:
- name: app
image: myapp/go-app:v1
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"]
terminationGracePeriodSeconds: 60
Python (Flask)
from flask import Flask, jsonify
import signal
import sys
import time
import threading
app = Flask(__name__)
is_shutting_down = False
# Count in-flight requests so shutdown can drain them
inflight_lock = threading.Lock()
inflight_requests = 0
@app.before_request
def _track_inflight_start():
global inflight_requests
with inflight_lock:
inflight_requests += 1
@app.teardown_request
def _track_inflight_end(exc=None):
global inflight_requests
with inflight_lock:
inflight_requests -= 1
@app.route('/healthz')
def healthz():
return jsonify({"status": "ok"}), 200
@app.route('/ready')
def ready():
if is_shutting_down:
return jsonify({"status": "shutting_down"}), 503
return jsonify({"status": "ready"}), 200
@app.route('/api/data')
def api_data():
if is_shutting_down:
return jsonify({"error": "service unavailable"}), 503
return jsonify({"data": "example"}), 200
def graceful_shutdown(signum, frame):
global is_shutting_down
print(f"Signal {signum} received, starting graceful shutdown")
# 1. Stop admitting new requests: readiness returns 503 so the load balancer stops traffic
is_shutting_down = True
# 2. Wait for in-flight requests to finish (up to 50 seconds)
deadline = time.time() + 50
while time.time() < deadline:
with inflight_lock:
if inflight_requests == 0:
break
time.sleep(0.5)
# 3. Cleanup tasks (for example, close database connections)
# db.close()
print("Graceful shutdown completed")
sys.exit(0)
# Register SIGTERM handlers
signal.signal(signal.SIGTERM, graceful_shutdown)
signal.signal(signal.SIGINT, graceful_shutdown)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
In production, use a WSGI server such as Gunicorn instead of the Flask development server (app.run). Gunicorn's master receives SIGTERM and shuts workers down gracefully at the worker level. The handler above is a development-server-level example; with Gunicorn, let --graceful-timeout and the worker shutdown behavior handle draining, and keep the application focused on flipping readiness and running cleanup tasks.
Deployment configuration:
apiVersion: apps/v1
kind: Deployment
metadata:
name: python-app
spec:
replicas: 3
selector:
matchLabels:
app: python-app
template:
metadata:
labels:
app: python-app
spec:
containers:
- name: app
image: myapp/python-flask:v1
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"]
terminationGracePeriodSeconds: 60