When a Spring Boot application is slow to start, it breaks your concentration every time you rebuild during local development, and on Kubernetes, Pods can be killed because they don’t pass the readiness probe in time. This article summarizes how to shorten startup time on a standard JVM environment without going as far as GraalVM Native Image.

Understand the Main Causes of Slow Startup

First, let’s figure out where the time is going. The areas that tend to be heavy during Spring Boot startup are roughly the following:

  • Classpath scanning and AutoConfiguration condition evaluation
  • Too many spring-boot-starter-* dependencies
  • DataSource and Hibernate initialization, waiting for external connections
  • Overly broad @ComponentScan ranges

Before making changes based on guesswork, the iron rule is to start with measurement.

Measure with —debug and ApplicationStartup

The quickest way to see the overall picture is to use the --debug option.

java -jar app.jar --debug

This outputs the CONDITIONS EVALUATION REPORT, showing which AutoConfigurations are enabled and why others were disabled.

When you want to take time measurements step by step, install BufferingApplicationStartup.

public static void main(String[] args) {
    SpringApplication app = new SpringApplication(MyApplication.class);
    app.setApplicationStartup(new BufferingApplicationStartup(2048));
    app.run(args);
}

The time taken for each step is recorded in the buffer, which you can retrieve via the Actuator described below. If you want to read it yourself without adding Actuator, another option is to stream FlightRecorderApplicationStartup to JFR.

Visualize via the Actuator startup Endpoint

Add spring-boot-starter-actuator and expose the startup endpoint.

management.endpoints.web.exposure.include=startup

After startup, hitting GET /actuator/startup returns a JSON list of steps. Sorting timeline.events[].duration in descending order immediately reveals the prime bottleneck. If you expose this in production, be sure to protect it with authorization or restrict it to your internal network.

Effects and Pitfalls of spring.main.lazy-initialization

The easiest measure is lazy initialization.

spring.main.lazy-initialization=true

The creation of all Beans is deferred until they are first used, so startup alone will definitely be faster. However, there are side effects:

  • The latency of the first request gets worse
  • Startup errors caused by configuration mistakes won’t surface until the first request in production

These are the pitfalls. A realistic approach is to enable it aggressively for local development and CI, while in production, narrow it down to individual Beans with @Lazy.

Exclude Unnecessary AutoConfigurations

Look at the Positive matches in the conditions evaluation report, and exclude any features you don’t use.

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration;
import org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration;

@SpringBootApplication(exclude = {
    JmxAutoConfiguration.class,
    WebSocketServletAutoConfiguration.class
})
public class MyApplication { }

You can also specify them all at once via properties.

spring.autoconfigure.exclude=\
  org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration

JMX, WebSocket, Mail, and Redis are typical examples of “included in dependencies but not used.” After excluding them, always run regression tests to confirm that no unexpected Beans have disappeared.

If you want to manage things centrally in code, use @SpringBootApplication(exclude=...). If you want to switch by profile or environment variable, use spring.autoconfigure.exclude — choosing based on the use case is recommended. For more on how AutoConfiguration itself works, see How Spring Boot AutoConfiguration Works.

Tidy Up Component Scanning and Dependencies

If you leave @SpringBootApplication sitting at the root package, it will scan all packages underneath. Just explicitly specifying basePackages has a fair effect.

@SpringBootApplication(scanBasePackages = "com.example.app.api")
public class MyApplication { }

Along with that, take inventory of the Starter dependencies in your pom.xml or build.gradle and remove unused ones — this lightens things up from the foundation.

Enable CDS in Spring Boot 3.3+

The JVM-standard Class Data Sharing speeds up class loading by pre-archiving class metadata into a shared archive. It has been officially supported since Spring Boot 3.3.

The procedure has two stages. First, perform one training run to create the archive.

java -XX:ArchiveClassesAtExit=app.jsa \
     -Dspring.context.exit=onRefresh \
     -jar app.jar

java -XX:SharedArchiveFile=app.jsa -jar app.jar

-Dspring.context.exit=onRefresh is the option that terminates the process immediately after the Application context’s refresh completes. Since we want to generate only the archive without starting the actual service, we specify this on training runs.

The shortening rate depends on the number of classes and the JDK version, so it’s hard to state definitively, but the Spring official blog and others report double-digit percentage improvements, and with virtually no side effects, this is a high-priority measure. When using it with Docker, don’t forget to include the generated app.jsa in the image.

AOT Mode Without GraalVM

Spring Boot lets you take advantage of AOT processing on a regular JVM without using GraalVM Native Image. It pre-generates Bean metadata and reduces reflection processing at startup.

At runtime, enable the following property.

spring.aot.enabled=true

However, this alone has no effect. At build time, you need to run the process-aot goal of spring-boot-maven-plugin for Maven, or the processAot task for Gradle, to generate the AOT artifacts in advance.

./mvnw spring-boot:process-aot
# For Gradle
./gradlew processAot

When AOT is enabled, some settings such as profiles are fixed at build time rather than at startup time, so note that it doesn’t play well with operations where profiles are switched at runtime. Combining it with CDS stacks the effects, so enabling both for production is the standard approach. For the differences from Native Image, also see How to Use GraalVM Native Image with Spring Boot.

Make Use of Startup Probes on Kubernetes

If startup still takes tens of seconds, secure some grace period on the Kubernetes side. By configuring a startupProbe, the livenessProbe judgment is suppressed until startup completes.

startupProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  failureThreshold: 30
  periodSeconds: 5
readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080
  periodSeconds: 10
livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  periodSeconds: 10

Using Actuator’s /health/liveness and /health/readiness allows you to reflect Spring Boot’s startup completion state directly in the probes. For how to use Actuator itself, refer to Getting Started with Spring Boot Actuator.

Run the Measure → Apply → Re-measure Cycle

Finally, a word on process. If you apply measures all at once, you won’t know what was effective and by how much, so always apply them one at a time. Empirically, the order of greatest effect is roughly as follows (the parenthetical references the corresponding headings in this article):

  1. Dependency cleanup and narrowing the Component scan range (“Tidy Up Component Scanning and Dependencies”)
  2. Excluding unnecessary AutoConfigurations (“Exclude Unnecessary AutoConfigurations”)
  3. Using CDS and AOT together (“Enable CDS in Spring Boot 3.3+”, “AOT Mode Without GraalVM”)
  4. Lazy initialization (“spring.main.lazy-initialization”, mainly for local/CI)

CDS/AOT is placed above lazy initialization because it has few side effects in production and works steadily. However, the order can shift depending on application structure, so measuring the baseline multiple times to take an average, and verifying the effect of one measure at a time, ends up being the shortest path after all.

Summary

For startup speedup, accumulating small improvements based on measurement ends up being more effective than flashy single measures. First, visualize the bottlenecks with Actuator’s startup endpoint, then try the order of dependency cleanup → AutoConfig exclusions → CDS/AOT. Even without going into GraalVM, you can aim for sufficiently practical reductions.