If your Spring Boot apps feel slow, the fix is usually not one magic JVM flag or a major database rewrite.
Most gains come from removing avoidable work, making startup leaner, and making sure the app behaves predictably under load.
That is why Spring Boot performance work usually breaks into a few familiar buckets:
- Startup time
- Memory and GC behavior
- Database access
- Request handling and thread usage
- Downstream I/O
- Scaling and deployment
Here is the practical version of what tends to work, based on my experience building Spring apps.

Don’t guess, measure
Before you change anything, answer this basic question: where is the time actually going?
For Spring Boot applications, a typical setup will use these tools:
- Spring Boot Actuator and Micrometer for metrics like request timing, memory, GC, and thread pool visibility
- Distributed tracing to find slow downstream calls
- Load testing with tools like k6, Gatling, or JMeter so you can reproduce the problem on demand
Spring has a nice blog post on adding telemetry (metrics, logs, and traces) to your apps here: https://spring.io/blog/2025/11/18/opentelemetry-with-spring-boot. If you haven’t incorporated telemetry into your app, start here.
Startup time
Spring Boot apps commonly suffer from slow startup time: classpath scanning, auto-configuration, and expensive bean creation.
Startup time is increasingly important in container platforms where pods restart frequently or scale out aggressively. Faster startup means faster recovery, faster deployments, and less time waiting for readiness.
The most common startup optimizations are:
- Examine your starters/dependencies and remove any that are unused. We’ve all added dependencies to a project and forgotten to remove them.
- A lot of starters pull in complex auto-configuration. Exclude any auto-configuration you don’t use with
spring.autoconfigure.excludeor directly in your main class:
@SpringBootApplication(exclude = { MyUnusedAutoConfiguration.class })
public class Application {
}- Narrow classpath scanning with like
@ComponentScanand@EntityScan. Note that this may change application behavior and should be used judiciously. - Mark non-critical beans as
@Lazywhen delayed initialization is acceptable. Set this per-bean, or globally:
spring.main.lazy-initialization=true- Use startup debug output to see what auto-configuration is doing
If startup speed is critical to your application, explore ahead-of-time compilation with GraalVM native images. GraalVM exhibits much faster startup time (milliseconds vs seconds) and generally lower memory use, but a more complex build and testing story. Some libraries or frameworks may behave differently on GraalVM.
Test JDK/Spring Boot upgrades before tuning the app
Sometimes the cheapest performance improvement involves upgrading your tools. Upgrading the JDK (looking at you, Java 25) may offer considerable speedups without changing a line of code.
Newer JDKs improve:
- Startup behavior
- Container awareness
- Garbage collection, including the default G1 garbage collector.
- Runtime performance, including heap management (See JEP 519; Compact Object Headers)
Spring Boot 3 and Spring Boot 4 performance features:
- Available since Spring Boot 3.2, you may run @Async methods, scheduled tasks, web requests and more on virtual threads. It’s also easy to enable, and well suited for concurrent I/O:
spring.threads.virtual.enabled=true- Spring Boot 4 leverages smaller starters (smaller app JARs), improves startup time, and lowers overall memory consumption.
- G1 remains a strong default GC for Spring apps.
Reduce database overhead before touching thread counts
In a lot of Java services, the database is still the biggest performance limiter. If your database isn’t optimized, you may waste time tuning Tomcat or the JVM while the application is still doing too many round-trips per request.
The usual suspects are not surprising:
- N+1 queries from ORM usage -> use fetch joins, custom queries, or batch fetching to avoid N+1 behavior
- missing or weak indexes -> review execution plans
- oversized entity loads when a DTO projection would do
- long transactions -> limit transaction scope to the immediate operation, or use advanced patterns like Sessionless Transactions
- chatty write patterns -> batch inserts and updates when possible, configurable in Spring properties
- a connection pool misconfigurations -> if you’re using Oracle, try UCP over Hikari for Oracle-native connection pooling
Tune downstream I/O
A Spring Boot service is usually only as fast as the systems it calls.
That means performance work should include:
- reusing HTTP clients instead of creating them per request
- enabling connection pooling
- setting clear connect and read timeouts
- using retries carefully
- adding circuit breakers where failure isolation matters
- batching or parallelizing downstream calls when the use case allows it
One of the most common throughput killers is a serial call chain: one request comes in, then the app makes several slow remote calls one after another. Even small improvements here can move latency a lot.
Scale horizontally on real metrics
Horizontal scaling is important, but it should not be the first answer to avoidable waste.
If a single instance starts slowly, over-allocates memory, blocks on remote calls, and runs poor queries, adding more replicas just spreads the inefficiency around.
The healthier pattern is:
- make one instance efficient
- keep the service stateless where possible
- externalize session state if horizontal scaling requires it
- autoscale on signals that reflect user pain, like latency, queue depth, or request rate, not just CPU
This is where startup time comes back into the conversation. Faster startup improves autoscaling behavior because new instances become useful sooner.
Final thoughts
The best-performing Spring Boot applications initialize less, allocate less, block less, query better, and expose telemetry for routine performance analysis. If you want a practical checklist, start here:
- measure the system with metrics, traces, and load tests
- remove obvious startup and dependency overhead
- fix database inefficiencies
- reduce wasted work in request handling
- validate JVM and GC settings
- scale out once the instance-level behavior is acceptable

Leave a Reply