How does server.shutdown=graceful work, and what role does spring.lifecycle.timeout-per-shutdown-phase play?
answer
- graceful: stop new, finish in-flight
- default is immediate
- timeout-per-shutdown-phase default 30s
- SIGTERM triggers; SIGKILL bypasses
- Undertow 503; others reject at network layer
basics
~10 sserver.shutdown=graceful makes the embedded web server stop accepting new requests but finish in-flight ones before shutting down. spring.lifecycle.timeout-per-shutdown-phase (default 30s) caps how long it waits for those requests before forcing shutdown.
solid answer
~50 sBy default Spring Boot shuts the embedded server down immediately, dropping in-flight requests. Setting `server.shutdown=graceful` changes that: when shutdown is triggered (context close, typically via SIGTERM and the JVM shutdown hook), the server **stops accepting new requests** and lets outstanding ones finish. `spring.lifecycle.timeout-per-shutdown-phase` (default 30s) is the grace period — the maximum time the shutdown phase waits for active requests to complete before proceeding to force termination. This is implemented through the `SmartLifecycle` phase machinery: the web server bean lives in the graceful-shutdown phase, so the timeout applies to it. Behavior of new requests during the window differs by server: Tomcat, Jetty, and Reactor Netty stop accepting at the network layer, while Undertow keeps accepting connections but returns HTTP 503. Crucially, graceful shutdown only helps if the process receives a graceful signal (SIGTERM), not a SIGKILL, and pairs with load-balancer/readiness deregistration to avoid dropped traffic.
code
java · 11 lines// application.yml
// server:
// shutdown: graceful # default is 'immediate'
// spring:
// lifecycle:
// timeout-per-shutdown-phase: 45s # default 30s; drain window
// Kubernetes must give the pod enough time and deregister first:
// spec.terminationGracePeriodSeconds: 60 # > drain window + preStop
// lifecycle.preStop.exec: ["sh","-c","sleep 10"] # let LB notice readiness=DOWN
// readinessProbe hitting /actuator/health/readiness flips to REFUSING_TRAFFIC on shutdowngo deeper
Know graceful = finish in-flight requests, stop taking new ones; it's off by default.
Know the timeout property, its 30s default, and what triggers shutdown (SIGTERM/context close).
Explain the SmartLifecycle phase mechanism and per-server rejection behavior (Undertow 503 vs network-layer).
Design the full zero-downtime deploy: readiness deregistration, preStop, terminationGracePeriodSeconds vs drain timeout, and SIGTERM guarantees.
### The default (immediate) behavior Out of the box the embedded web server (Tomcat/Jetty/Undertow/Reactor Netty) stops **immediately** when the application context closes. Any requests being processed at that instant are dropped — clients see connection resets/errors. In a rolling deployment or autoscaling event this causes user-visible failures. ### Enabling graceful shutdown Set `server.shutdown=graceful` (default is `immediate`). Now, when shutdown begins, the server enters a **grace period** during which: 1. It **stops accepting new requests**. 2. It lets **in-flight requests run to completion**. 3. After they finish (or the timeout elapses), the server and context finish shutting down. ### What triggers shutdown? Graceful shutdown runs when the `ApplicationContext` is closed. In practice: - The **JVM shutdown hook** Spring Boot registers fires on `SIGTERM` (e.g. `kill`, Kubernetes pod termination, `docker stop`, Ctrl-C). - Calling the Actuator `/actuator/shutdown` endpoint (if enabled), or `context.close()` programmatically. - **`SIGKILL` (kill -9) bypasses everything** — no graceful shutdown, no hooks. Same for OOM kills. - Note: some IDEs' stop button sends a signal that doesn't allow graceful shutdown; test with a real SIGTERM. ### The timeout: `spring.lifecycle.timeout-per-shutdown-phase` Spring's lifecycle stops beans in **phases** via the `SmartLifecycle` contract (higher phase = stopped earlier). The embedded web server is placed in the graceful-shutdown `SmartLifecycle` phase (via `WebServerGracefulShutdownLifecycle`). `spring.lifecycle.timeout-per-shutdown-phase` (an ISO-8601/duration value, **default 30s**) bounds how long each shutdown phase — including the web server's — waits for its beans to stop. If in-flight requests haven't drained within that window, the shutdown proceeds anyway and remaining requests may be terminated. Set it to comfortably exceed your slowest legitimate request (e.g. `45s`), but keep it under your orchestrator's kill grace period (e.g. Kubernetes `terminationGracePeriodSeconds`, default 30s) or the platform will SIGKILL you mid-drain. ### Per-server behavior of rejected requests During the grace window, new requests are handled differently per server (from the Spring Boot reference): - **Tomcat, Jetty, Reactor Netty** — stop accepting new requests at the **network layer** (new connections aren't accepted). - **Undertow** — continues to accept connections but responds with **HTTP 503 (Service Unavailable)** immediately. ### Production coordination (the real gotcha) Graceful shutdown alone is not enough. During shutdown the instance may still be in the load balancer's rotation. Proper flow: 1. **Deregister / flip readiness to DOWN** first (Spring Boot publishes `AvailabilityChangeEvent` → `ReadinessState.REFUSING_TRAFFIC` on shutdown; Kubernetes readiness probe should fail so the LB stops sending traffic). 2. Wait for the LB to notice (a `preStop` sleep in Kubernetes is a common trick). 3. Then let graceful shutdown drain in-flight requests. 4. Ensure `terminationGracePeriodSeconds` > `timeout-per-shutdown-phase` + preStop delay so the pod isn't SIGKILLed early. ### Summary of key properties - `server.shutdown=graceful` — enable it (default `immediate`). - `spring.lifecycle.timeout-per-shutdown-phase=30s` — drain timeout per phase. - Actuator readiness / `ReadinessState` — for LB deregistration.
- You set server.shutdown=graceful but requests are still dropped during Kubernetes rollouts. What's likely wrong?Traffic isn't being deregistered before draining, or terminationGracePeriodSeconds is shorter than timeout-per-shutdown-phase so the pod gets SIGKILLed mid-drain. Fail the readiness probe first (REFUSING_TRAFFIC), add a preStop delay so the LB stops routing, and make the grace period exceed the drain timeout. Also confirm the signal is SIGTERM, not SIGKILL.
- What is the default value of spring.lifecycle.timeout-per-shutdown-phase?30 seconds. It bounds how long each SmartLifecycle shutdown phase (including the web server draining in-flight requests) waits before proceeding.
saying these in an interview costs you the question
- Believing graceful shutdown is the default (it's 'immediate')
- Thinking SIGKILL still allows graceful draining
- Assuming graceful shutdown alone prevents dropped traffic without LB/readiness deregistration
- Setting timeout-per-shutdown-phase larger than the orchestrator's kill grace period
- Saying all servers return 503 during the window (only Undertow does; others reject at the network layer)