server.shutdown
Chooses between graceful shutdown, which lets in-flight requests finish, and immediate shutdown; graceful is the default in Spring Boot 4.1.
server.shutdown=graceful- Added in Spring Boot Spring Boot 4.1 (verified present)
Parameters
| Parameter | Type | Description |
|---|---|---|
$valueoptional | Shutdown= graceful | graceful or immediate. |
$spring.lifecycle.timeout-per-shutdown-phaseoptional | Duration= 30s | Longest wait for each shutdown phase, including the graceful wait for active requests. Requests still running after it are cut off. |
Return value
n/aHow the embedded web server stops when the application context closes: graceful stops accepting new requests and waits for active ones; immediate closes at once.
When a deployment replaces an instance, the platform sends the process SIGTERM. What happens to requests that are halfway through is decided by server.shutdown. With graceful, the server stops accepting new requests, waits for the active ones to finish, and only then lets the rest of the context close. With immediate, connections are closed at once and in-flight requests fail.
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=20sIn Spring Boot 4.1 graceful is already the default. Older guides tell you to switch it on because earlier versions defaulted to immediate; it does no harm to set it explicitly so the intent is visible.
The timeout
The wait is bounded by spring.lifecycle.timeout-per-shutdown-phase, 30 seconds by default. Requests still running when it expires are cut off. Set it a little above your slowest legitimate request, and make sure the platform's own grace period is longer still. On Kubernetes terminationGracePeriodSeconds defaults to 30; if Spring waits 30 seconds as well, the kubelet can send SIGKILL before the context has finished closing.
Requests that arrive after SIGTERM
Graceful shutdown handles requests already inside the application. It cannot stop a load balancer from sending new ones. On Kubernetes, removing a pod from the Service endpoints happens in parallel with SIGTERM, so for a few seconds traffic can still arrive at a server that has stopped accepting it, and clients see connection errors or 502s. A short preStop sleep delays the signal until the endpoint change has spread.
Background work
Only the web server's requests are covered by this property. Message listeners, schedulers and your own threads stop as their beans are destroyed. Make long-running work stop cleanly: implement SmartLifecycle or use @PreDestroy to finish or hand back the current item, and keep each unit small enough to complete within the timeout.
None of this runs on SIGKILL (kill -9) or when the JVM crashes. Design consumers and handlers to be safe to retry, because some shutdowns will not be graceful.
Example
# Default in Spring Boot 4.1, set explicitly so nobody wonders.
server.shutdown=graceful
# Slowest legitimate request plus a margin; keep it below the platform's grace period.
spring.lifecycle.timeout-per-shutdown-phase=20s
When to use it
- Rolling deployments on Kubernetes without failed requests at each pod replacement
- Letting long report downloads finish when an instance is scaled in
- Shutting down a queue-polling worker without abandoning a half-processed batch
Frequently asked questions
Is graceful shutdown on by default in Spring Boot?
Yes. In Spring Boot 4.1 server.shutdown defaults to graceful, with each shutdown phase limited to 30 seconds by spring.lifecycle.timeout-per-shutdown-phase.
Why do clients still get errors during a rolling deployment?
Usually the load balancer keeps sending new requests for a moment after SIGTERM, when the server has stopped accepting them. Add a short preStop sleep, and check that the platform's grace period exceeds the shutdown timeout.
More examples
Kubernetes settings that match the application
The preStop sleep gives the Service time to stop routing to the pod before SIGTERM arrives. 5 seconds of sleep plus 20 seconds of graceful wait fits inside the 40 second grace period with room to spare.
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-api
spec:
template:
spec:
terminationGracePeriodSeconds: 40
containers:
- name: app
image: registry.example.com/orders-api:1.4.0
ports:
- containerPort: 8080
lifecycle:
preStop:
sleep:
seconds: 5
Stopping a background worker cleanly
server.shutdown only covers HTTP requests. A SmartLifecycle bean takes part in the same phased shutdown, so the worker finishes its current batch, within its own bound, instead of being abandoned halfway.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.SmartLifecycle;
import org.springframework.stereotype.Component;
@Component
public class ImportWorker implements SmartLifecycle {
private static final Logger log = LoggerFactory.getLogger(ImportWorker.class);
private final AtomicBoolean running = new AtomicBoolean();
private ExecutorService executor;
@Override
public void start() {
running.set(true);
executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
while (running.get()) {
importNextBatch();
}
});
}
@Override
public void stop() {
running.set(false);
executor.shutdown();
try {
if (!executor.awaitTermination(15, TimeUnit.SECONDS)) {
log.warn("Import batch did not finish in time; it will be retried on the next start");
executor.shutdownNow();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Override
public boolean isRunning() {
return running.get();
}
private void importNextBatch() {
try {
Thread.sleep(1_000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Seeing the difference locally
The two log lines confirm graceful shutdown is in effect. Trying it with a deliberately slow endpoint is the quickest way to check the timeout and the platform's grace period before relying on them in production.
# Start the app, then call a slow endpoint in another terminal
java -jar target/orders.jar &
APP_PID=$!
sleep 15 # wait until the log shows "Started"
curl -s http://localhost:8080/api/reports/slow &
sleep 1 # let the request reach the server
# SIGTERM: the slow request completes, then the JVM exits
kill -TERM "$APP_PID"
# log: Commencing graceful shutdown. Waiting for active requests to complete
# log: Graceful shutdown complete
# SIGKILL would skip all of it: kill -9 "$APP_PID"
Discussion