Graceful Shutdown and Health Checks in Java

Khimananda Oli 8 min read Programming and Languages
Graceful Shutdown and Health Checks in Java

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.

Load BalancerReadiness ProbeSIGTERM SignalJVM ExitActive Request ProcessingDrain Phase (timeout-per-shutdown)New Requests Rejected After Readiness FailsExisting Requests Complete Before JVM Exit
Sequence of graceful shutdown and health checks in Java showing request draining between readiness failure and SIGTERM

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.

CriteriaLiveness ProbeReadiness Probe
PurposeDetects unrecoverable deadlock or corruptionDetermines if pod can accept traffic
Failure ActionKubelet kills and restarts the containerPod removed from Service endpoints
Should Check DB?No — transient DB outage ≠ app deathYes — app cannot serve without dependencies
Startup BehaviorUse startupProbe or high initialDelaySecondsFails until app fully initialized
Graceful Shutdown RoleMust NOT fail during drain phaseMUST 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.

Health Check FailureIs App Deadlocked / Corrupt?YESNOLIVENESS: Restart PodREADINESS: Remove from LBCheck: Memory, Thread Lock,JVM Crash, Fatal Config ErrorCheck: DB Connection, Cache,Downstream API, Queue Consumer
Decision flowchart distinguishing liveness from readiness probes for graceful shutdown and health checks in Java

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: close headers 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.

Stale ConnectionsUpstream reuses closed TCP connFix: Connection: close headeror HTTP/2 GOAWAY frameSlow Endpoint Propagationkube-proxy/mesh lag > preStopFix: Increase preStop sleepValidate via access log timestampsBlocking Shutdown Hooks@PreDestroy exceeds timeoutFix: Async cleanup + profilingLog hook start/end timestampsSymptom: RST packetsin tcpdump post-shutdownSymptom: 502s cluster-wideduring first 10s of deploySymptom: SIGKILL aftergrace period expiryValidation: Zero 5xx in access logs across 10 consecutive rolling updatesMonitor shutdown_duration_seconds histogram + readiness_probe_failures_total
Comparison of three common failure modes preventing successful graceful shutdown and health checks in Java deployments

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.

Frequently Asked Questions

Set server.shutdown=graceful in application.properties. This tells the embedded Tomcat or Jetty server to stop accepting new requests while allowing active requests to complete within the configured timeout period before forcing termination.

Thirty seconds.

Use /actuator/health/liveness for liveness probes and /actuator/health/readiness for readiness probes. These distinct endpoints prevent unnecessary pod restarts during startup or temporary dependency failures while ensuring traffic only routes to fully initialized instances.

SIGKILL immediately terminates the process without cleanup. Graceful shutdown handles SIGTERM, allowing Spring contexts to close, database connections to release, and in-flight requests to finish processing before the JVM exits cleanly.

Yes. Override spring.lifecycle.timeout-per-shutdown-phase using environment variables or profile-specific configuration files. Production environments often require longer timeouts than development to accommodate complex transaction rollbacks and external service disconnections during deployment cycles.

Readiness probes often fail because beans initialize asynchronously or database migrations run post-startup. Implement a custom ReadinessState indicator that only returns UP after all critical initialization tasks complete, preventing load balancers from routing traffic prematurely.

Yes. Virtual threads in Java 21+ respect standard shutdown hooks and Spring lifecycle events. However, ensure blocking operations use proper interruption handling, as virtual threads can mask resource leaks if cancellation tokens are not propagated correctly during shutdown sequences.

Send SIGTERM via kill command or docker stop. Monitor logs for context closure messages and verify active requests complete. Use curl against the health endpoint to confirm it returns DOWN status immediately after receiving the termination signal.

They complete normally.

Readiness checks should verify critical dependencies to prevent routing traffic to broken instances. Liveness checks must remain lightweight and avoid external calls, as transient network issues or database latency could trigger unnecessary container restarts and cascading failures across your cluster.

Register a SmartLifecycle bean that stops listener containers before the web server shuts down. This ensures message acknowledgment completes and prevents duplicate processing. Configure consumer prefetch limits appropriately so in-flight messages drain within your allocated shutdown timeout window.

Minimal. Standard actuator endpoints add negligible overhead when properly cached and secured. Avoid expensive queries in liveness probes. Use async health indicators for readiness checks involving slow dependencies to prevent blocking the main request thread pool during high-traffic periods.

Restrict access using network policies or ingress rules rather than authentication, since kubelets cannot easily pass credentials. Expose only necessary endpoints via management.endpoints.web.exposure.include. Never expose heap dumps or environment details on public-facing health check paths.

Non-daemon threads, unclosed resources, or synchronous cleanup logic exceeding the configured phase timeout. Profile shutdown duration using JFR or logging. Refactor blocking operations into asynchronous tasks with explicit cancellation support to ensure predictable termination within allocated time budgets.

No. Readiness probes signal individual instance health but do not coordinate cluster-wide traffic shifting. Combine them with preStop hooks that sleep briefly, allowing load balancers and service meshes to propagate endpoint removal before the application stops accepting connections.