Java 25 Startup Performance for Spring Boot, Quarkus, and Micronaut
09 Oct 2025
The release of Java 25 (including GraalVM) and recent years of Java startup performance improvements inspired me to re-evaluate choices in Java web frameworks in low-usage scenarios such as scale-to-zero apps, where Java’s slow startup is a problem. The “industry standard” Spring Boot has improved considerably since I first started using it while alternative frameworks like Quarkus and Micronaut have also matured greatly. I want to check how fast these can start and how much memory they need as a baseline.
In this post I compare the startup time and memory usage for these 3 frameworks in a simple application using JDBC, Flyway, and Postgres. The database has a single table and the application serves both a JSON API and a server-side rendered HTML via a templating engine. I used what I felt are “typical” libraries for each framework based on its common tutorials and project generation tool defaults, aiming to capture “typical” usage rather than what is possible with targeted optimizing. For example, Spring Boot is usually lighter with Undertow instead of the Tomcat default, while Micronaut and Quarkus already default to lighter-weight engines (Netty and Vert.x). In Micronaut, I used Micronaut Data JDBC versus Hibernate in Spring Boot and Quarkus.
Full details are provided later in the post, but as it’s long I’ll start with the results as run on an M2 Pro Macbook with 16GB of memory and GraalVM 25:




Quarkus JVM | Micronaut JVM | Spring Boot JVM | Quarkus Native | Micronaut Native | Spring Boot Native | |
---|---|---|---|---|---|---|
Startup (sec) | 1.154 | 0.656 | 1.909 | 0.049 | 0.050 | 0.104 |
Heap Used MB | 14.4 | 17.6 | 28.2 | 3.2 | 6.0 | 11.0 |
Heap Allocated MB | 46.1 | 50.3 | 86.0 | 8.4 | 19.1 | 59.8 |
Max RSS | 271.2 | 253.2 | 388.9 | 70.5 | 83.8 | 149.4 |
Since I’m only considering low-usage scenarios, I did not test for scalability or total throughput. Also, if running in a small container in the cloud in a scale-to-zero environment, they will start up slower with fewer CPUs, but I expect the relative performance to be close. If I have time I will do a followup in a true cloud environment.
Results
The results are in line with reports online from other developers and the frameworks themselves. Native services start much faster and use less memory. The build-time configuration from Quarkus and Micronaut give great benefits even when running on JVM. In native mode, given the configuration used, Quarkus is a clear winner over Micronaut, which is interesting given that Micronaut was not using Hibernate as ORM and I expected it to be more light-weight.
It would be interesting to try to make each of the 3 frameworks as equal as possible and compare how each library choice changes the results, for example if Quarkus used Thymeleaf, or if I used Spring Data JDBC and Undertow in Spring Boot. I am also curious how this might compare to using something like http4k or Vert.X directly.
The start times for Spring Boot is impressive from a historical standpoint, showing how far Spring and JVM startup has improved over the years.
My conclusion is that all the frameworks are good enough after native compilation for a scale-to-zero service, assuming the cloud service can launch the container quickly enough, which I hope to test in the future.
Other Notes
The test results are for the “production” mode which doesn’t include the framework’s dev tools. From a developer experience perspective, nothing beats the depth of documentation and online resources of Spring Boot. I found the documentation for Quarkus and Micronaut both quite workable. Micronaut was the easiest for me to understand. However, Micronaut doesn’t have a “live reload”; the best I found is to run gradle in a continuous build mode so it restarts when code changes. This is still pretty fast, but not as fast as Spring Boot and Quarkus. In the area of dev tools, Quarkus dev services can start a Postgres and OIDC server (Keycloak) and many other services for you when your application starts, can run unit tests continuously, and provides an extensive dev dashboard to change configuration properties and logging on the fly. You can do similar things with Spring Boot, but with a bit more configuration
Regarding native building, the Spring Boot application took longer and more memory (I had an out of memory error once) compared to Quarkus and Micronaut; however, I didn’t measure exactly. The default native build configuration from the project generators was used.
Test Details
Each implementation provides the same functionality to create and retrieve people from a simple “person” table with a sequence-generated id, name and age. I didn’t implement update or delete.
GET /
returns a static page with a form using HTMX that does aPOST /person
and places the resulting fragment in the page.GET /heap
callsSystem.gc()
and returns atext/plain
response with “used” and “committed” heap usage from MemoryMXBeanGET /person
returns all people in JSON or HTML, depending on the Accept header.GET /person/{id}
returns a single person in JSON or HTML.POST /person
takes JSON and returns JSON orapplication/x-www-form-urlencoded
and returns a small HTML fragment.
The HTML is generated using the chosen template engine (Thymeleaf or Qute) except the POST /person
generates a
small success fragment HTML as a String.
The application was also required to expose and generate an OpenAPI specification. All the frameworks supported a Swagger UI as well.
Testing
I built the application into either a “far jar” (JVM mode) or into the native executable using the standard process for the framework and build system. Then I ran and immediately shutdown the application 4 times. I took the average startup time reported by the framework for the last 3 runs. The first run I discarded to ensure the OS file system cache is consistently “hot”. In real-world scenarios the JVM mode is likely impacted a lot more for “cold” starts since the disk size is greater and there are multiple files, further amplifying the benefits of the native builds.
To measure the memory usage, I ran the application an additional time with /usr/bin/time -l
then ran an IntelliJ HTTP
client script to load all the framework components. I pulled the heap results from the GET /help
endpoint and the
maximum resident set size from /usr/bin/time -l
.
- Adds a person by posting JSON to
/person
- Gets all people in JSON
- Gets all people in HTML
- Gets the heap statistics
Hardware and Software Configuration
For all three frameworks, the JVM, database version and hardware are the same:
Common Component | Version |
---|---|
MacOS | 15.7 |
CPU | M2 Pro 16GB |
Postgres | 17.6 |
JVM | Oracle GraalVM 25+37.1 |
I created a new, separate, Postgres database locally for each service.
As mentioned in the summary, I used what I felt was the most “default” or “common” option. In the below table you can see these choices in detail. In some cases, like the HTTP engine I did not make an explicit choice so that the default from the starter would be used. The exception being Flyway and Postgres that I wanted to keep in common as a choice external to the application and were used per the framework’s guides. The Flyway SQL file and database schema were identical for all 3 solutions. I created the schema and initial data entirely via Flyway and not via the ORM.
Component | Spring Boot | Micronaut | Quarkus |
---|---|---|---|
Framework Version | 3.5.6 | 4.5.4 | 3.28.1 |
Build System | Maven | Gradle Kotlin | Maven |
Schema Management | Flyway | Flyway | Flyway |
ORM | Spring Data JPA / Hibernate | Micronaut Data JDBC | Panache / Hibernate |
Connection Pool | Hikari | Hikari | quarkus-agroal |
Serialization | Jackson | Micronaut Serialization | Jackson (quarkus-rest-jackson) |
HTTP engine | Tomcat | Netty | Vert.x |
Web Framework | Spring MVC | Default built-in | quarkus-rest |
Template Engine | Thymeleaf | Thymeleaf | Qute |
Developer Tools | Spring DevTools | Gradle continuous build | Built-in |
OpenAPI | springdoc-openapi | micronaut-openapi | quarkus-smallrye-openapi |
Note: even though the index page used HTMX, the HTMX is loaded from CDN on browser side and no framework integrations with HTMX are used so it had no impact on the results.
Schema
create sequence person_seq start with 1 increment by 50;
CREATE TABLE person
(
id INTEGER PRIMARY KEY default nextval('person_seq'),
name VARCHAR(255) NOT NULL,
age INTEGER
);