Back to Blog
spring-bootkubernetesdevopsjavaproductionmicroservices

Spring Boot Graceful Shutdown in Kubernetes: Don't Drop Requests on Deploy (2026)

A rolling deployment without graceful shutdown drops in-flight requests. Learn Spring Boot graceful shutdown, preStop hooks, connection draining, and Kubernetes readiness probe configuration.

J

JOptimize Team

May 28, 2026· 8 min read

Without graceful shutdown, a Kubernetes rolling deployment works like this: Kubernetes removes the pod from the load balancer and simultaneously sends SIGTERM. Active requests are cut off mid-execution. Users see 502 errors, transactions roll back, and Kafka consumers lose their partition assignments mid-batch. This is avoidable.


Spring Boot Graceful Shutdown

Enabled with one property (Spring Boot 2.3+):

# application.properties server.shutdown=graceful spring.lifecycle.timeout-per-shutdown-phase=30s # Wait up to 30s for active requests

With this configuration, on SIGTERM:

  1. Spring Boot stops accepting new requests (readiness probe returns DOWN)
  2. Waits for active requests to complete (up to 30 seconds)
  3. Calls @PreDestroy methods and DisposableBean.destroy()
  4. Closes database connections and Kafka consumers cleanly
  5. JVM exits

Kubernetes Configuration

# deployment.yaml spec: strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 # Never have fewer than desired pods maxSurge: 1 # One extra pod during rollout template: spec: terminationGracePeriodSeconds: 60 # Kubernetes waits 60s before SIGKILL containers: - name: order-service image: myapp:latest lifecycle: preStop: exec: command: ["sleep", "10"] # Wait for load balancer to drain

The preStop sleep is critical. Without it:

Kubernetes sends SIGTERM → Spring Boot stops accepting requests
BUT: load balancer still routes to this pod for a few seconds
→ New requests arrive, get rejected → 502 errors

With preStop: sleep 10:

preStop hook runs (10s) → load balancer drains → SIGTERM sent → Spring graceful shutdown

Readiness and Liveness Probes

# application.properties management.endpoint.health.probes.enabled=true management.health.livenessstate.enabled=true management.health.readinessstate.enabled=true
# deployment.yaml livenessProbe: httpGet: path: /actuator/health/liveness port: 8081 initialDelaySeconds: 60 # Give app time to start periodSeconds: 10 failureThreshold: 3 # Kill pod after 3 consecutive failures readinessProbe: httpGet: path: /actuator/health/readiness port: 8081 initialDelaySeconds: 20 periodSeconds: 5 failureThreshold: 3 # Remove from load balancer after 3 failures

During graceful shutdown, Spring Boot automatically sets readiness to OUT_OF_SERVICE — Kubernetes removes the pod from the load balancer immediately, before the shutdown timeout.


Custom Readiness Indicator

@Component public class KafkaConsumerReadinessIndicator implements ReactiveHealthIndicator { private final KafkaListenerEndpointRegistry kafkaRegistry; @Override public Mono<Health> health() { boolean kafkaRunning = kafkaRegistry.getListenerContainers() .stream() .allMatch(c -> c.isRunning()); return Mono.just(kafkaRunning ? Health.up().build() : Health.down().withDetail("reason", "Kafka consumers not running").build()); } }

This custom indicator makes the pod not-ready until Kafka consumers are fully started — preventing traffic during startup before consumers are warmed up.


Graceful Kafka Consumer Shutdown

@Component @RequiredArgsConstructor public class KafkaShutdownHook { private final KafkaListenerEndpointRegistry registry; @EventListener(ContextClosingEvent.class) public void onContextClosing() { log.info("Context closing — stopping Kafka consumers"); registry.getListenerContainers() .forEach(container -> container.stop()); // Finish current batch, then stop log.info("Kafka consumers stopped"); } }

This stops Kafka consumers cleanly before the Spring context closes, allowing the current batch to complete and commit offsets before shutdown.


Database Connection Cleanup

@Component @RequiredArgsConstructor public class DatabaseShutdownHook { private final HikariDataSource dataSource; @PreDestroy public void shutdown() { log.info("Closing HikariCP connection pool"); dataSource.close(); // Waits for active connections, then closes pool log.info("Connection pool closed"); } }

HikariCP's .close() waits for all active connections to be returned to the pool before closing — no active DB transactions are cut mid-flight.


Startup vs Shutdown Sequence

STARTUP (Kubernetes readiness controls traffic):
1. JVM starts
2. Spring context initializes (beans, DB connection pool)
3. Flyway migrations run
4. Kafka consumers start
5. readiness → UP → Kubernetes adds pod to load balancer
6. Traffic starts flowing

SHUTDOWN (Zero downtime):
1. Kubernetes calls preStop hook → sleep 10s
2. Load balancer drains connections from this pod
3. SIGTERM sent → Spring sets readiness → DOWN
4. No new requests accepted
5. Active requests complete (up to 30s)
6. Kafka consumers stop cleanly
7. DB connections close
8. JVM exits

Common Mistakes to Avoid

  • terminationGracePeriodSeconds shorter than timeout-per-shutdown-phase — if Kubernetes kills the pod before Spring finishes graceful shutdown, requests are still cut; terminationGracePeriodSeconds must be > preStop sleep + shutdown timeout
  • No preStop hook — load balancer continues routing to the pod for a few seconds after SIGTERM; without the preStop delay, new requests get rejected during this window
  • Liveness probe too aggressive — a liveness probe that kills a pod during a temporary GC pause causes unnecessary restarts; set failureThreshold=3 with periodSeconds=10 minimum
  • Not testing graceful shutdown — send load to the service, trigger a rolling deploy, and verify zero 502s with a tool like hey or k6

Summary

Zero-downtime Kubernetes deployments require four things: server.shutdown=graceful in Spring Boot, preStop: sleep 10 to drain the load balancer before SIGTERM, readiness probes that go DOWN during shutdown, and terminationGracePeriodSeconds larger than your total shutdown window. Together, this makes rolling deploys invisible to users.


Detect Deployment Configuration Issues

JOptimize flags missing graceful shutdown configuration, absent liveness/readiness probe setup, and connection pool configurations not tuned for containerized environments.

Deploy without dropping requests — free configuration scan.

Want to go deeper?

Master Spring Boot, security, and Java performance with hands-on courses.

Detect issues in your project

JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.