Gillius's Programming

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:

Graph of Java Heap size in megabytes Graph of Java Heap size in megabytes Graph of Java Heap size in megabytes Graph of Java Heap size in megabytes
  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.

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.

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
);
Dev Version