framework-benchmarks
Three identical REST APIs - Spring Boot, Quarkus, Micronaut - built JVM and native, and benchmarked for cold-start time, resident memory, and throughput. Companion repo for the ankurm.com post "Spring Boot 4.1 vs Quarkus vs Micronaut: Startup, Memory and Throughput Benchmarked."
No docs/ chapter folder here by design - every finding below is written up in full
in the post itself, inside per-section <details> accordions. This repo carries the
runnable code and the raw captured output those sections link to.
Versions under test
| Component | Version | Verified against |
|---|---|---|
| Spring Boot | 4.1.1 | spring-boot-dependencies Maven Central metadata |
| Quarkus | 3.40.1 (the 3.40 LTS stream) | quarkus-bom Maven Central metadata + quarkus.io/releases |
| Micronaut | 5.2.2 | micronaut-platform Maven Central metadata |
| GraalVM | CE 25.4.4.1.1 (OpenJDK 25.0.4.1.1) | graalvm/graalvm-ce-builds GitHub releases (latest at build time) |
| JDK baseline | 25 (current LTS) | all three apps compiled and run on the same GraalVM JDK 25 |
Quickstart
# JVM mode
cd spring-boot-app && mvn -q package -DskipTests && java -jar target/spring-boot-app.jar # :8081
cd quarkus-app && mvn -q package -DskipTests && java -jar target/quarkus-app/quarkus-run.jar # :8082
cd micronaut-app && mvn -q package -DskipTests && java -jar target/micronaut-app.jar # :8083
# Native mode (needs GraalVM's native-image on PATH; budget ~7-8 min per app)
cd spring-boot-app && mvn -q -Pnative package -DskipTests && mvn -q -Pnative native:compile && ./target/spring-boot-app
cd quarkus-app && mvn -q -Pnative clean package -DskipTests && ./target/quarkus-app-runner
cd micronaut-app && mvn -q -Pnative clean package -DskipTests \
&& mvn -q -Pnative package org.graalvm.buildtools:native-maven-plugin:compile -DskipTests \
&& ./target/micronaut-app
scripts/run-all.sh does all of the above plus every benchmark, end to end, and
regenerates every file in docs/output/.
Modules
| Module | Framework | JVM port | Build profile for native |
|---|---|---|---|
spring-boot-app |
Spring Boot 4.1.1, Tomcat, Spring MVC | 8081 | -Pnative, then explicit native:compile (see below) |
quarkus-app |
Quarkus 3.40.1, Quarkus REST (RESTEasy Reactive), Vert.x | 8082 | -Pnative |
micronaut-app |
Micronaut 5.2.2, Netty, Micronaut Serialization (compile-time, not reflection) | 8083 | -Pnative + explicit native-maven-plugin:compile (see below) |
All three expose the identical endpoints, same JSON shapes, same seed data, no database:
| Method | Path | Behaviour |
|---|---|---|
GET |
/widgets |
list all widgets (seeded with 5) |
GET |
/widgets/{id} |
one widget, 404 if unknown |
POST |
/widgets |
create ({"name": "...", "quantity": N}), 400 on blank name or negative quantity |
GET |
/health (Micronaut) / /actuator/health (Spring Boot) / /q/health (Quarkus) |
trivial liveness check - deliberately each framework's own normal way to add one, not forced to be identical, since that difference is itself one of the post's findings |
No database on purpose: it isolates framework/runtime startup overhead from JDBC driver and native-image reflection-metadata concerns, which is a separate, larger topic than this post covers.
The two real build failures this repo hit (and the fixes, committed as-is)
-
Micronaut silently ignored
application.yml.micronaut-yaml(which wraps snakeyaml) isn't a transitive dependency ofmicronaut-http-server-netty- without it, Micronaut skips YAML config files with no warning and falls back to every default (here, port 8080 instead of the configured 8083). Fixed by usingapplication.propertiesinstead, matching the other two modules. See the post's Micronaut section for the full story. -
Micronaut's native-image build failed four times in a row on a Netty/logback static-initialization clash before
--initialize-at-run-time=io.netty(the whole package) fixed it. All four realnative-imageerror messages are indocs/output/06-micronaut-native-build-failures.txt, verbatim. The working build argument lives inmicronaut-app/pom.xml'snativeprofile.
Also note: neither native-maven-plugin nor Spring Boot's own native profile
auto-binds native-image to a Maven phase in a plain multi-module layout the way
mvn -Pnative package alone implies - both modules need the goal invoked explicitly
(native:compile, or Micronaut's native-maven-plugin:compile). See scripts/run-all.sh
for the exact commands that actually trigger a native build end to end.
Captured output
| File | What it is |
|---|---|
docs/output/01-startup-memory-jvm.txt |
All 3 frameworks, JVM mode, 4 cold-start runs each, real measure.py JSON |
docs/output/02-throughput-jvm.txt |
All 3 frameworks, JVM mode, 15s/20-concurrency throughput runs |
docs/output/03-startup-memory-native.txt |
Same as 01, native mode |
docs/output/04-throughput-native.txt |
Same as 02, native mode |
docs/output/05-summary.txt |
Consolidated table, hand-written from the four files above |
docs/output/06-micronaut-native-build-failures.txt |
The four real native-image build attempts for Micronaut, verbatim |
scripts/measure.py and scripts/throughput.py are dependency-free Python (stdlib
only) - no wrk/hey/ab install required to reproduce any number in this repo.
License
MIT