
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Dropped requests during rolling deployments remain a primary source of user-facing errors in Java microservices, even with modern orchestration. Implementing graceful shutdown and health checks in Java correctly bridges the gap between application lifecycle events and infrastructure signals, ensuring in-flight transactions complete before a pod terminates. This guide covers the exact Spring Boot configuration, Kubernetes probe tuning, and shutdown hook ordering required for production reliability.
server.shutdown=graceful in Spring Boot, configuring a spring.lifecycle.timeout-per-shutdown-phase, and aligning Kubernetes preStop hooks with readiness probes to drain active requests before SIGTERM reaches the JVM.How Do You Configure Graceful Shutdown and Health Checks in Java Spring Boot?
Spring Boot 3.x provides first-class support for graceful shutdown, but it is disabled by default. Without explicit configuration, receiving a SIGTERM signal immediately stops accepting new connections and aborts in-flight requests, causing HTTP 502/503 errors at the load balancer. To enable proper graceful shutdown and health checks in Java, you must configure both the web server shutdown mode and the actuator health endpoints.
Enable Graceful Shutdown in application.yml
The core configuration requires two properties: one to enable graceful mode and another to define the maximum wait time for active requests to complete. This timeout must be shorter than your container orchestrator's termination grace period but long enough for your slowest legitimate request.
# application.yml
server:
shutdown: graceful
port: 8080
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
management:
endpoint:
health:
probes:
enabled: true
show-details: always
endpoints:
web:
exposure:
include: health,info,prometheus The server.shutdown=graceful property tells Tomcat (or Jetty/Undertow) to stop accepting new connections while allowing existing requests to finish. The timeout-per-shutdown-phase sets a hard ceiling; if requests haven't completed within this window, they are forcibly terminated. For most transactional APIs processing database writes or external calls, 30 seconds is a reasonable starting point. If you have batch endpoints or file upload handlers that legitimately take longer, increase this value accordingly, but always keep it below your Kubernetes terminationGracePeriodSeconds.
Register Custom Shutdown Hooks for Non-HTTP Resources
Spring Boot's graceful shutdown only manages HTTP request draining. If your application consumes messages from Kafka, processes RabbitMQ tasks, or maintains WebSocket connections, you need explicit shutdown hooks to pause consumers and acknowledge pending work. A common mistake in production incidents I've debugged is assuming graceful shutdown handles message queue consumers automatically—it does not.
@Component
public class KafkaConsumerShutdownHook {
private final KafkaListenerEndpointRegistry registry;
public KafkaConsumerShutdownHook(KafkaListenerEndpointRegistry registry) {
this.registry = registry;
}
@PreDestroy
public void onStop() {
// Pause all listeners to stop fetching new records
registry.getListenerContainers().forEach(container -> {
container.pause();
});
// Allow in-flight records to complete processing
// The container will commit offsets before closing
}
} This pattern ensures your message consumers stop pulling new records when shutdown begins, while the Kafka listener container finishes processing any records already fetched. Without this, rebalancing during deployment causes duplicate processing or lost acknowledgments. Apply similar logic for Redis pub/sub subscribers, scheduled tasks via @Scheduled, and custom thread pools.
What Is the Difference Between Liveness and Readiness Probes in Kubernetes?
Misunderstanding probe semantics is the single most frequent cause of cascading failures in Java deployments on Kubernetes. Both probes hit your actuator endpoints, but they trigger fundamentally different platform behaviors. Getting this wrong means either restarting healthy pods unnecessarily or routing traffic to pods that cannot serve requests.
| Criteria | Liveness Probe | Readiness Probe |
|---|---|---|
| Purpose | Detects unrecoverable deadlock or corruption | Determines if pod can accept traffic |
| Failure Action | Kubelet kills and restarts the container | Pod removed from Service endpoints |
| Should Check DB? | No — transient DB outage ≠ app death | Yes — app cannot serve without dependencies |
| Startup Behavior | Use startupProbe or high initialDelaySeconds | Fails until app fully initialized |
| Graceful Shutdown Role | Must NOT fail during drain phase | MUST fail immediately on SIGTERM/preStop |
The critical distinction for graceful shutdown and health checks in Java is that readiness must fail before the JVM stops processing requests, while liveness must remain passing throughout the entire shutdown sequence. If liveness fails during shutdown, Kubernetes restarts the container mid-drain, killing in-flight requests. This is why you should never include downstream dependency checks in liveness probes—a database connection pool exhaustion is a readiness problem, not a liveness problem.
How Do You Align Kubernetes Probes with Spring Boot Shutdown Timing?
The race condition between Kubernetes removing a pod from service endpoints and the application stopping request acceptance is where most zero-downtime deployments fail. Even with graceful shutdown enabled, there is a window where the load balancer still routes traffic to a pod that has begun shutting down. Closing this gap requires precise alignment of probe configuration, preStop hooks, and termination grace periods.
Configure Probes and PreStop Hook
Your Kubernetes deployment manifest must coordinate three timing values. The preStop hook executes before SIGTERM and should introduce a small delay to allow endpoint propagation. Readiness must fail immediately when shutdown begins. Liveness must tolerate the entire drain duration.
spec:
terminationGracePeriodSeconds: 60
containers:
- name: java-app
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 5"]
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 1
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
startupProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
failureThreshold: 30
periodSeconds: 2 The 5-second preStop sleep is non-negotiable in practice. When Kubernetes marks a pod as terminating, it simultaneously removes the pod from endpoints and sends the preStop hook. However, kube-proxy/iptables rules update asynchronously across nodes. Without this buffer, some nodes continue routing traffic for 1–3 seconds after the pod enters shutdown, hitting a server that has already stopped accepting connections. For deeper context on how these signals interact with observability, see the four golden signals of monitoring to understand which metrics reveal probe misalignment.
Tune Termination Grace Period Math
Your terminationGracePeriodSeconds must satisfy this inequality:
terminationGracePeriodSeconds > preStop + timeout-per-shutdown-phase + buffer
With a 5s preStop, 30s shutdown timeout, and 5s buffer, you need at least 40s. Setting it to 60s provides headroom for slow GC pauses or network delays. If the grace period expires before the JVM exits cleanly, Kubernetes sends SIGKILL—immediate termination with no cleanup. Monitor actual shutdown durations via process_uptime_seconds metrics and adjust. Teams managing stateful services like databases should also review PostgreSQL administration essentials since Java apps holding open DB connections during unclean shutdown can leave locks or prepared transactions behind.
Why Does My Java Application Still Drop Requests During Rolling Updates?
If you have configured everything above and still observe intermittent 502 errors during deployments, the issue typically falls into one of three categories. These are the failure modes I encounter most frequently in production audits.
Client-Side Connection Pool Staleness
Your Java service may shut down gracefully, but upstream callers (other microservices, API gateways, or external clients) maintain persistent HTTP/1.1 connections to the old pod IP. When that connection is reused after the pod terminates, the caller gets a connection reset. Solutions include:
- Setting
Connection: closeheaders during the shutdown drain phase via a servlet filter - Configuring client-side connection pools with max-idle-time shorter than your deployment interval
- Using HTTP/2, which handles GOAWAY frames natively during server shutdown
Insufficient Readiness Propagation Delay
The 5-second preStop sleep assumes your cluster's endpoint propagation latency is under 5 seconds. In large clusters with many services, or when using service mesh sidecars like Envoy/Istio, propagation can take 8–12 seconds. If you're running a service mesh, consult Istio service mesh fundamentals for mesh-specific drain configuration. Increase the preStop sleep and validate by checking access logs for requests arriving after the readiness probe first failed.
Blocking Shutdown Hooks Exceeding Timeout
A @PreDestroy method that synchronously flushes a large cache, waits for a distributed lock, or performs an unbounded database operation will block the shutdown sequence beyond your configured timeout. Profile your shutdown hooks with logging timestamps. Any hook taking more than 5 seconds is suspect. Make cleanup asynchronous where possible, or move non-critical cleanup to background threads with their own timeout.
Implementing Reliable Graceful Shutdown and Health Checks in Java
Production-grade graceful shutdown and health checks in Java demand treating application lifecycle and infrastructure signals as a unified system, not separate concerns. Enable server.shutdown=graceful, distinguish liveness from readiness rigorously, add a preStop sleep, and validate with real deployment tests—not just local curl commands. Measure success by zero 5xx responses across rolling updates, monitored through the observability patterns covered in Prometheus metrics monitoring fundamentals. If your team needs help auditing your current Java deployment pipeline or designing compliance-ready shutdown procedures, reach out to discuss your architecture.