When Kubernetes stops a pod, it sends the process a SIGTERM signal. By default, Spring Boot handles it abruptly: it stops the server immediately, cutting off every in-flight HTTP request. Clients get a 502 Bad Gateway or a Connection reset. This is exactly the problem graceful shutdown solves.
A clean shutdown is not a single setting but a coordinated sequence: flip readiness → give the load balancer time to remove the pod from rotation → drain in-flight requests → close connections → terminate. In this article we walk through the minimal set of Spring settings that make this happen.
server.shutdown=graceful — the foundation of everything
Without this setting, on SIGTERM Spring calls Tomcat.stop() immediately. All in-flight requests are aborted with an IOException, and clients see a 502.
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
With server.shutdown: graceful the behavior changes:
- Tomcat stops accepting new connections.
- HTTP requests already in flight keep being processed.
- Once all requests finish (or the timeout expires), Tomcat shuts down completely.
This is what a "graceful" shutdown means.
How to choose the timeout
spring.lifecycle.timeout-per-shutdown-phase sets how many seconds Spring waits for each shutdown phase to complete. The default is 30 seconds. It's better to set it explicitly, so it's visible in the configuration and doesn't drift between Spring Boot versions.
How to pick a value:
- Under 20 seconds — may not be enough to drain Tomcat's threads under load. Long requests get cut off.
- Over 45 seconds — risky in Kubernetes: if the overall
terminationGracePeriodSecondsbudget is 60 seconds, the JVM may get SIGKILL before it finishes. - 30 seconds — a reasonable balance for most REST APIs, where 99% of requests complete within a few seconds.
If you have requests that inherently take longer than 30 seconds, the answer is to decompose them into shorter operations, not to increase the timeout.
ApplicationAvailability — how k8s learns you're shutting down
Kubernetes decides whether to route traffic to a pod based on the result of the readiness probe — an HTTP request to /actuator/health/readiness. As long as it returns UP, new requests keep going to the pod, even if it's already shutting down.
Spring Boot 2.3+ introduces ApplicationAvailability — a mechanism that ties the application's lifecycle state to the response of the health endpoints. On receiving a shutdown signal, Spring automatically switches readiness to REFUSING_TRAFFIC, and /actuator/health/readiness starts returning 503.
Kubernetes sees the 503 from the readiness probe and removes the pod from Service routing — new traffic stops arriving. After that, Spring waits for the already in-flight requests to finish and shuts down.
To have an explicit log entry for the moment of the switch, you can add a listener:
@Component
@RequiredArgsConstructor
@Slf4j
public class ShutdownAvailabilityListener {
private final ApplicationEventPublisher events;
@EventListener(ContextClosedEvent.class)
public void onShutdown() {
log.info("Graceful shutdown started, switching readiness to REFUSING_TRAFFIC");
events.publishEvent(new AvailabilityChangeEvent<>(this, ReadinessState.REFUSING_TRAFFIC));
}
}
Spring Boot does this automatically, but an explicit listener gives you visibility in the logs and a safeguard in case the defaults change between versions.
Separate probes for liveness and readiness
By default, Spring Boot exposes a single /actuator/health endpoint for everything. Kubernetes needs separate endpoints:
management:
endpoint:
health:
probes:
enabled: true
show-details: when-authorized
health:
livenessstate:
enabled: true
readinessstate:
enabled: true
After this you get two separate endpoints:
/actuator/health/liveness— the process is alive (the JVM hasn't hung, no deadlock)./actuator/health/readiness— ready to accept traffic (depends onApplicationAvailability).
This distinction matters during shutdown. It's specifically readiness that needs to return 503 — then Kubernetes removes the pod from rotation. If you return 503 from liveness, Kubernetes will decide the process has hung and restart it instead of waiting for a clean shutdown.
A common mistake: a custom shutdown flag
Sometimes developers add their own AtomicBoolean shuttingDown or a static volatile boolean isShuttingDown field and flip it on shutdown:
// Don't do this
@Component
public class CustomShutdownState {
private static final AtomicBoolean SHUTTING_DOWN = new AtomicBoolean(false);
@EventListener(ContextClosedEvent.class)
public void onShutdown() {
SHUTTING_DOWN.set(true);
}
public static boolean isShuttingDown() {
return SHUTTING_DOWN.get();
}
}
The problem is that the health endpoints know nothing about this flag. /actuator/health/readiness keeps returning UP, Kubernetes doesn't remove the pod from rotation, and traffic keeps arriving throughout the entire shutdown.
The right approach is to use ApplicationAvailability directly:
@Component
@RequiredArgsConstructor
public class SomeService {
private final ApplicationAvailability availability;
public void doWork() {
if (availability.getReadinessState() == ReadinessState.REFUSING_TRAFFIC) {
log.info("Refusing new work, shutdown in progress");
return;
}
// work
}
}
This way the shutdown state is coordinated through a single mechanism that is visible to both the health endpoints and Kubernetes.
In short
server.shutdown: gracefulis mandatory — without it Tomcat aborts in-flight requests immediately on SIGTERM.timeout-per-shutdown-phase: 30sis a good balance for typical REST APIs; don't raise it above 45s in Kubernetes with a 60-second budget.- Spring Boot 2.3+ automatically switches readiness to
REFUSING_TRAFFICon shutdown; an explicit listener is useful for visibility in the logs. - Separate
livenessandreadinessprobes are needed: readiness=503 removes the pod from rotation, liveness=503 restarts it. - A custom
AtomicBoolean shuttingDownis a classic mistake: it doesn't integrate with the health endpoints, so Kubernetes never sees the shutdown.
Further reading
- HTTP drain — what happens to in-flight HTTP requests during shutdown.
- Kafka shutdown — how to cleanly stop a Kafka listener container.
- Kubernetes — the preStop hook and terminationGracePeriodSeconds.
- Budgets and observability — how to allocate the 60-second shutdown budget.