Spring Boot vs Quarkus on a 768 MiB VM
An earlier 512 MiB experiment compared Spring Boot and Quarkus under constrained JVM settings. This follow-up asks how the same small application shape behaves with a little more host memory and updated framework versions.
Question
How do equivalent Spring Boot and Quarkus applications behave when they share a 768 MiB host and the same constrained JVM settings?
Setup
The experiment ran in an Ubuntu virtual machine with the following configuration:
| Component | Configuration |
|---|---|
| Operating system | Ubuntu 24.04, x86_64 |
| CPU | 1 vCPU |
| Memory | 768 MiB configured; approximately 703.5 MiB visible inside the guest |
| Swap | 256 MiB configured |
| Java, both applications | Eclipse Temurin 25.0.4.1+1 |
| Spring Boot | 4.1.1 |
| Quarkus | 3.39.4, JVM mode |
| Monitoring | StatLite 0.4.2, published Linux x86_64 release |
| Application storage | Separate file-backed H2 databases |
Both applications used the same JVM options:
-Xms16m
-Xmx80m
-Xss256k
-XX:+UseSerialGC
-XX:TieredStopAtLevel=1
-XX:ReservedCodeCacheSize=32m
-XX:+UseCompactObjectHeaders
The 80 MiB maximum heap is only one part of a Java process's memory use. RSS also includes resident memory used by the JVM, loaded code, thread stacks, and other allocations.
Each application implements a small Star Pulse-style dashboard for the same three GitHub repositories. They have equivalent dashboard functionality, separate H2 files, and bounded database pools. Their HTTP, rendering, and metrics implementations remain specific to their frameworks.
Both applications use JPA and Hibernate: Spring through Spring Data JPA, and Quarkus through Hibernate ORM with Panache.
A Python standard-library HTTP fixture on loopback supplied fixed GitHub-like responses. This removed live GitHub response variability from the run. Both applications were configured to begin repository polling after 300 seconds and repeat it every 300 seconds. The fixture and monitoring tool also consumed resources on the measured VM.
Run
The controlled sequence was:
- Start the fixture, then Spring Boot, then Quarkus, waiting for application readiness.
- Record the first memory checkpoint, send one deliberate
GET /to each application, and record the next checkpoint. - Start StatLite monitoring of Spring, Quarkus, and StatLite itself at a 30-second polling interval.
- Send ten paired requests: one
GET /to Spring and one to Quarkus per pair, with a one-second pause after each pair. - Record the post-workload checkpoint and observe the services for approximately ten minutes, taking additional snapshots.
Readiness probes, endpoint checks during snapshots, and monitoring requests contributed additional traffic. The workload counts below refer specifically to the ten deliberate workload requests per application. Framework request counters were not used to compare those counts.
The post-workload observation anchor was 2026-09-18T23:49:45Z. The final snapshot began at 2026-09-19T00:00:09Z, 10 minutes 24 seconds later. This was a manually controlled short observation; checkpoint names describe nominal stages rather than exact sampling deadlines.
Results
RSS is process resident memory. Process swap is the VmSwap value recorded for each application's PID. Both were read from Linux process status data; values below are MiB, converted from the recorded KiB values by dividing by 1,024.
| Checkpoint | Time (UTC) | Spring RSS | Spring swap | Quarkus RSS | Quarkus swap |
|---|---|---|---|---|---|
| Ready, before deliberate request | 23:46:30 | 228.2 | 0.0 | 164.6 | 0.0 |
| After one deliberate request | 23:47:13 | 223.9 | 4.5 | 163.8 | 0.0 |
| After workload | 23:49:35 | 221.3 | 7.6 | 164.4 | 0.0 |
| Intermediate | 23:51:40 | 210.1 | 25.3 | 172.1 | 0.0 |
| Approximately 5 minutes | 23:55:00 | 208.3 | 27.6 | 173.5 | 0.0 |
| Approximately 10 minutes | 00:00:09, September 19 | 202.3 | 34.3 | 175.7 | 0.0 |
Process RSS (MiB)
Process swap (MiB)
Spring's RSS was approximately 26.6–63.6 MiB higher than Quarkus across these snapshots. By the final checkpoint, Spring had accumulated 34.3 MiB of process swap; Quarkus recorded zero process swap at every checkpoint.
Observations
Spring's resident memory declined while its process swap increased. A declining RSS alongside increasing swap should not be interpreted as an equivalent reduction in the application's overall memory demand.
At the final snapshot, the host reported approximately 123.4 MiB of available memory and 81.8 MiB of total swap use. Total host swap includes other processes; it is distinct from the process swap shown in the table.
All ten workload requests per application returned HTTP 200, with a stable response hash within each application. Spring, Quarkus, and StatLite were active with zero recorded restarts at the final checkpoint. StatLite's final summary showed successful latest polls and zero consecutive poll failures for all three targets. The collected kernel and service failure searches returned no matching entries.
Dashboard views
These closeout screenshots show the monitored application and host dashboards. They provide operational context; the checkpoint table and chart above remain the primary measured evidence.
Limitations
For this application pair, shared JVM profile, startup order, and host configuration, Quarkus had a smaller resident footprint at every sampled checkpoint. Spring accumulated process swap while continuing to serve requests successfully. These observations describe one run under light traffic.
The experiment has several limits:
- It is one approximately ten-minute observation. It does not establish long-term stability or a steady memory state.
- Spring started first. Startup order was not alternated, and the services shared a host, so the result does not isolate each application's independent memory requirement.
- The applications implement equivalent functionality through different framework stacks. The comparison is specific to these implementations and versions.
- The workload contains only ten deliberate requests per application. It does not establish throughput, latency, or behavior under sustained load.
- RSS and swap were sampled at checkpoints. Zero recorded process swap means zero at those samples, rather than proof about every instant between them.
- The fixture and StatLite contributed host memory use and traffic. The setup reflects monitored applications sharing a constrained VM.
- The earlier 512 MiB experiment had a longer observation and different framework versions. Its final measurements are not directly equivalent to these ten-minute checkpoints.
1 GiB follow-up
The same applications and workload were subsequently run on a 1 GiB VM with swap disabled. Final RSS was 240.4 MiB for Spring and 181.2 MiB for Quarkus. Quarkus remained lower at every sampled checkpoint. Both applications completed the workload and remained active with zero recorded restarts.
RAM and swap configuration both changed, so this comparison does not isolate the effect of extra RAM. See the 1 GiB experiment and comparison for the full results and limitations.
Planned follow-up
In a separate Spring Boot JPA vs JDBC experiment, the small JDBC application used about 90 MiB less RSS than the JPA/Hibernate variant. Those measurements were made on a Mac with different conditions, so the saving cannot be assumed for this VM or for Quarkus.
I plan to rerun this comparison without JPA and Hibernate, using JDBC-based application templates. That follow-up will measure how a simpler persistence stack changes the memory footprint under the same constrained host and JVM settings.
Evidence
The public experiment package contains the method, checkpoint data, workload record, and final-checkpoint polling summary. Monitoring was provided by StatLite.
The results here use the recorded RSS and process swap. The CSV's legacy spring_heap_bytes and quarkus_heap_bytes fields contain generation-specific values from the capture parser, rather than total JVM heap use, and are not used for this comparison. The package also includes a separate 1 GiB follow-up; the table above uses only the original 768 MiB run.
1 GiB no-swap follow-upQuestions and commentsStatLite on GitHub