Graceful Shutdown in Spring Boot: Mechanisms, Code Samples, and Configuration
This article explains how Spring Boot handles graceful shutdown in Kubernetes, manual Actuator shutdown, SIGTERM handling, Tomcat graceful termination, and Logback log flushing, providing detailed code examples, configuration snippets, and the underlying shutdown hook mechanisms that ensure resources are released cleanly.
When a Kubernetes pod stops, it first sends a SIGTERM signal to the Spring Boot process, allowing it to shut down gracefully within the terminationGracePeriodSeconds (default 30 seconds). If the application does not exit in time, Kubernetes sends SIGKILL to force termination.
Manually, you can trigger shutdown by calling the Actuator endpoint /actuator/shutdown , which closes the WebApplicationContext and then exits.
kill -TERM Method
Spring Boot invokes methods annotated with @PreDestroy during graceful shutdown. Example:
@PreDestroy
public void cleanup() {
// execute cleanup operations
log.info("Received shutdown event. Performing cleanup and shutting down gracefully.");
}Sending SIGTERM triggers the cleanup() method, where you can see the thread name SpringApplicationShutdownHook in the logs.
The shutdown hook registers with the JVM via Runtime.getRuntime().addShutdownHook(new Thread(this, "SpringApplicationShutdownHook")) , allowing custom tasks to run before the JVM exits.
During SpringApplication#run() , before refreshing the application context, the shutdown hook is added to the JVM.
ShutdownEndpoint Method
Expose the Actuator shutdown endpoint by adding to application.yml :
management:
endpoint:
shutdown:
enabled: true
endpoints:
web:
exposure:
include: '*'The endpoint starts a new thread that calls this.context.close() , which in turn triggers the SpringApplicationShutdownHook .
SpringBoot Tomcat Graceful Shutdown
By default, Spring Boot stops Tomcat immediately. Adding server.shutdown=GRACEFUL to the configuration makes Tomcat wait for active requests to finish before shutting down.
server:
shutdown: GRACEFULWhen server.shutdown=IMMEDIATE is set, Tomcat stops abruptly, causing errors such as ClosedChannelException .
With graceful shutdown, Tomcat logs messages like "Commencing graceful shutdown. Waiting for active requests to complete" and pauses the protocol handler before finally destroying it.
The core of Tomcat graceful shutdown is the Connector#close() method, which stops accepting new connections and closes the server socket gracefully.
public class TomcatWebServer implements WebServer {
private final Tomcat tomcat;
private final GracefulShutdown gracefulShutdown;
public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {
this.tomcat = tomcat;
this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(tomcat) : null;
initialize();
}
@Override
public void shutDownGracefully(GracefulShutdownCallback callback) {
if (this.gracefulShutdown == null) {
callback.shutdownComplete(GracefulShutdownResult.IMMEDIATE);
return;
}
this.gracefulShutdown.shutDownGracefully(callback);
}
}The GracefulShutdown class creates a separate thread that pauses connectors, closes server sockets, and repeatedly checks whether all Tomcat contexts have become inactive, aborting if the timeout is exceeded.
final class GracefulShutdown {
private final Tomcat tomcat;
private volatile boolean aborted = false;
void shutDownGracefully(GracefulShutdownCallback callback) {
logger.info("Commencing graceful shutdown. Waiting for active requests to complete");
new Thread(() -> doShutdown(callback), "tomcat-shutdown").start();
}
private void doShutdown(GracefulShutdownCallback callback) {
List
connectors = getConnectors();
connectors.forEach(this::close);
try {
for (Container host : this.tomcat.getEngine().findChildren()) {
for (Container context : host.findChildren()) {
while (isActive(context)) {
if (aborted) { callback.shutdownComplete(GracefulShutdownResult.REQUESTS_ACTIVE); return; }
Thread.sleep(50);
}
}
}
} catch (InterruptedException ex) { Thread.currentThread().interrupt(); }
logger.info("Graceful shutdown complete");
callback.shutdownComplete(GracefulShutdownResult.IDLE);
}
private void close(Connector connector) { connector.pause(); connector.getProtocolHandler().closeServerSocketGraceful(); }
}Logback Graceful Shutdown to Ensure No Log Loss
Two common ways to improve logging performance are disabling immediate flush on OutputStreamAppender and using AsyncAppender . Both can lose logs on pod termination, so a shutdown hook is required.
Spring Boot automatically registers a Logback shutdown handler that calls getLoggerContext().stop() when the JVM receives SIGTERM . This stops all appenders, flushes buffers, and closes output streams.
public class LogbackLoggingSystem extends Slf4JLoggingSystem {
public Runnable getShutdownHandler() {
return () -> getLoggerContext().stop();
}
}The Logger root object detaches and stops all attached appenders, invoking each appender's stop() method. For OutputStreamAppender , stop() flushes and closes the underlying stream.
public class OutputStreamAppender
extends UnsynchronizedAppenderBase
{
@Override
public void stop() {
lock.lock();
try {
closeOutputStream();
super.stop();
} finally {
lock.unlock();
}
}
protected void closeOutputStream() {
if (this.outputStream != null) {
try {
encoderClose();
this.outputStream.close();
this.outputStream = null;
} catch (IOException e) {
addStatus(new ErrorStatus("Could not close output stream for OutputStreamAppender.", this, e));
}
}
}
}For AsyncAppender , the shutdown handler waits for the worker thread to flush the queue (default 1 s). If the timeout is exceeded, remaining events may be discarded.
public class AsyncAppenderBase
extends UnsynchronizedAppenderBase
implements AppenderAttachable
{
public static final int DEFAULT_MAX_FLUSH_TIME = 1000;
int maxFlushTime = DEFAULT_MAX_FLUSH_TIME;
@Override
public void stop() {
if (!isStarted()) return;
super.stop();
worker.interrupt();
try {
worker.join(maxFlushTime);
if (worker.isAlive()) {
addWarn("Max queue flush timeout (" + maxFlushTime + " ms) exceeded. Approximately " + blockingQueue.size() + " queued events were possibly discarded.");
} else {
addInfo("Queue flush finished successfully within timeout.");
}
} catch (InterruptedException e) {
int remaining = blockingQueue.size();
addError("Failed to join worker thread. " + remaining + " queued events may be discarded.", e);
}
}
}By leveraging these shutdown hooks, Spring Boot ensures that both the application and its logging subsystem terminate cleanly without losing in‑flight requests or log entries.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.