server.shutdown

Chooses between graceful shutdown, which lets in-flight requests finish, and immediate shutdown; graceful is the default in Spring Boot 4.1.

Signature
server.shutdown=graceful
  • Added in Spring Boot Spring Boot 4.1 (verified present)

Parameters

ParameterTypeDescription
$valueoptionalShutdown= gracefulgraceful or immediate.
$spring.lifecycle.timeout-per-shutdown-phaseoptionalDuration= 30sLongest 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=20s

In 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

Example · properties
# 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.

Example · yaml
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.

Example · java
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.

Example · bash
# 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

  • Be the first to comment on this lesson.