Enabling server.shutdown=graceful in Spring Boot 3.2: Who Owns the Drain Deadline?
0 reputation · 28 Sept 2025, 18:53 UTC
0 reputation · 28 Sept 2025, 18:53 UTC
Spring Boot 2.3 introduced graceful shutdown for every embedded server: with server.shutdown=graceful, Tomcat, Jetty, Undertow, and Netty stop accepting new requests while in-flight work runs until spring.lifecycle.timeout-per-shutdown-phase (default 30 seconds) expires. Migrating a small Spring Boot 3.2 service to this setting behind Kubernetes exposes a coordination gap the framework leaves open.
The timeout governs only the application side. Kubernetes removes a pod from Service endpoints asynchronously and runs the preStop hook before SIGTERM, so a sleep there is the usual way to let deregistration propagate before the grace window starts. That sleep is a heuristic: it is not tied to in-flight request count, endpoint propagation varies by provider, and deregistration from Eureka or Consul is not coordinated with the phase start.
Either the application owns the deadline — short timeout, longer orchestrator sleep — or the infrastructure does, with a minimal preStop and generous timeout. Each choice fails differently for long uploads versus fast failover targets.
Three open questions:
spring.lifecycle.timeout-per-shutdown-phase be derived from terminationGracePeriodSeconds minus the preStop sleep, or tuned independently of the cluster?35275 reputation · 29 Sept 2025, 03:34 UTC
The infrastructure (Kubernetes) must own the absolute deadline, while the application (Spring Boot) owns the execution window. Because Kubernetes issues a SIGKILL immediately after terminationGracePeriodSeconds expires, the Spring spring.lifecycle.timeout-per-shutdown-phase must be strictly less than the remaining time in the pod's lifecycle.
The Spring phase timeout should be derived from the orchestrator's limits. If you use a preStop sleep to allow for endpoint propagation, the calculation is:
Spring Timeout < (terminationGracePeriodSeconds - preStop sleep duration)
Tuning these independently of the cluster creates a race condition: if the Spring timeout is longer than the remaining Kubernetes grace period, the JVM is killed abruptly (Exit Code 137), preventing the application from completing its internal cleanup or releasing resources cleanly.
There is no native, first-class configuration in Spring Boot 3.2 to export the current in-flight request count as a signal to the Kubernetes preStop hook. The preStop hook remains a heuristic. To observe remaining requests, you must implement a custom Actuator endpoint or a Micrometer gauge that tracks active requests, though this cannot be used as a synchronous signal to delay the SIGTERM.
The spring.lifecycle.timeout-per-shutdown-phase bounds the entire shutdown phase of the application context. For Spring Cloud Gateway, this includes the time Netty takes to drain connections. Requests held inside long-lived filters are subject to this timeout; once the phase expires, the context is forced closed, and the underlying Netty server is shut down, severing those requests.
To verify which layer is terminating your requests, use the following method:
terminationGracePeriodSeconds to 30 and spring.lifecycle.timeout-per-shutdown-phase to 20.If the request is severed at 20 seconds with a "Commencing destruction of bean" log sequence, Spring is owning the deadline. If the pod disappears at 30 seconds without a clean Spring shutdown log, Kubernetes is owning the deadline via SIGKILL.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.