Spring Boot’s graceful shutdown isn’t about stopping gracefully; it’s about preventing new requests from being processed while actively finishing the ones already in flight.
Let’s watch it happen. Imagine a simple Spring Boot app with a REST endpoint that simulates a long-running task:
@SpringBootApplication
@RestController
public class GracefulShutdownApp {
public static void main(String[] args) {
SpringApplication.run(GracefulShutdownApp.class, args);
}
@GetMapping("/process")
public String processRequest() throws InterruptedException {
System.out.println("Received request, starting long process...");
Thread.sleep(10000); // Simulate 10 seconds of work
System.out.println("Finished long process.");
return "Processed successfully!";
}
}
Now, start this app. In one terminal, hit the endpoint:
curl http://localhost:8080/process
You’ll see "Received request, starting long process…" printed. Immediately, in another terminal, send the shutdown signal:
./mvnw spring-boot:stop
or if you’re running a JAR directly:
# Find the PID of your Spring Boot app
PID=$(pgrep -f your-app.jar)
# Send SIGTERM (graceful shutdown signal)
kill -15 $PID
Observe the output in the first terminal. You’ll see "Finished long process." printed before you get the shutdown confirmation. The application didn’t immediately die; it waited for the Thread.sleep(10000) to complete. If you had sent kill -9 $PID (SIGKILL), the "Finished long process." message would likely not appear, and the curl command would eventually time out.
This behavior is managed by Spring Boot’s embedded web server (like Tomcat, Netty, or Undertow) and Spring’s ApplicationContext. When you send a shutdown signal (like SIGTERM), Spring Boot initiates a shutdown hook. This hook tells the embedded server to stop accepting new connections and to wait for a configurable amount of time for existing requests to complete.
The core mechanism is the spring.lifecycle.timeout-per-shutdown-phase property. This isn’t a single timeout for the whole shutdown but rather a duration that applies to specific phases. The most critical phase for request draining is the CONTAINER_STOP phase. By default, this phase has a timeout of 30 seconds.
Here’s how you configure it:
In application.properties:
spring.lifecycle.timeout-per-shutdown-phase.CONTAINER_STOP=15s
In application.yml:
spring:
lifecycle:
timeout-per-shutdown-phase:
CONTAINER_STOP: 15s
This tells the container (e.g., Tomcat) to wait for a maximum of 15 seconds for active requests to finish. If a request started processing and it’s going to take longer than 15 seconds, it will be interrupted. You can also set a global timeout if you don’t need phase-specific control, though the phase-specific approach is generally more granular and recommended:
spring.lifecycle.timeout=15s
This global setting is equivalent to setting spring.lifecycle.timeout-per-shutdown-phase.*=15s.
The eureka.instance.non-eureka.shutdown-quiet-period property is sometimes mentioned in the context of shutdown, but it’s specific to Eureka client behavior and controls how long the client waits to announce its departure from Eureka after the application has started shutting down. It’s not directly related to draining web requests.
The real magic happens in the org.springframework.boot.web.embedded.tomcat.TomcatGracefulShutdown (or equivalent for Netty/Undertow) class. When a shutdown signal is received, it registers a ServletContextListener. This listener, in turn, signals the embedded server’s Lifecycle to transition to STOPPING. During this transition, the server’s connector is set to acceptingTraffic(false), preventing new requests. The awaitTermination() method on the server’s executor service is then invoked, waiting for active tasks to complete up to the configured timeout.
If you’re using a custom ThreadPoolTaskExecutor for your web server, you might need to explicitly register a SmartLifecycle bean to manage its shutdown. Spring Boot’s auto-configuration for embedded servers handles this for you. However, if you have custom async processing outside the web server’s direct control, you’ll need to ensure those tasks are also managed gracefully.
The trickiest part is when you have asynchronous operations, message consumers, or background threads that aren’t directly tied to an incoming HTTP request. Spring Boot’s graceful shutdown primarily targets the web server’s request processing threads. For other long-running tasks, you need to explicitly signal them to shut down. This often involves using ExecutorService.shutdown() and ExecutorService.awaitTermination() within your own SmartLifecycle beans, ensuring they respect the application’s overall shutdown lifecycle.
The next thing you’ll likely encounter is ensuring that background tasks, like message queue consumers, also shut down gracefully without losing messages.