JVM vs Native: Performance Trade-offs in Spring Boot and Quarkus for Constrained Environments
#Backend

JVM vs Native: Performance Trade-offs in Spring Boot and Quarkus for Constrained Environments

Backend Reporter
10 min read

A detailed analysis comparing Spring Boot on JVM versus Quarkus native compilation across different concurrency levels, revealing how runtime strategies impact resource efficiency, scalability, and operational resilience.

JVM vs Native: Performance Trade-offs in Spring Boot and Quarkus

In financial systems with aggressive cost optimization strategies, operational constraints often demand more than just raw performance. When deploying on spot instances that can be terminated at any time, three factors become critical: fast startup, resource efficiency, and predictable performance under constrained CPU and memory conditions.

While exploring alternatives within the Java ecosystem for these environments, I conducted a comprehensive comparison between the traditional JVM approach using Spring Boot and the native compilation approach using Quarkus with GraalVM. This analysis examines how these fundamentally different runtime strategies impact behavior under various concurrency levels.

Test Methodology and Architecture

The test architecture simulated a realistic financial services workload with the following components:

  • HTTP POST request processing: Each request published a message to Kafka, which was then consumed and persisted to PostgreSQL
  • Hardware: Apple Mac Mini M4 (ARM64) with 24GB RAM running macOS
  • Containerization: Docker with 1 CPU per service
  • Memory constraints: Spring Boot allocated 256MB, Quarkus Native allocated 128MB
  • Monitoring stack: Prometheus, Grafana, and Micrometer (for both frameworks)

The comparison intentionally avoids an apples-to-apples framework evaluation. Instead, it examines how runtime strategy—traditional JVM execution versus native compilation—affects performance characteristics.

Important Disclaimer

This is not a pure framework speed comparison but a comparison between two different runtime strategies:

  1. Spring Boot running on the JVM
  2. Quarkus compiled to a GraalVM native image

The purpose is not to declare a winner but to understand how runtime choices impact performance, resource usage, and scalability in real-world conditions.

Startup Time Comparison

Before stress testing, cold startup time was measured inside the container:

  • Quarkus Native (3.31.1): 0.118 seconds
  • Spring Boot 3.5.7 (JVM): Root WebApplicationContext initialized in 1492 ms (~1.49 seconds)

This represents roughly a 12× faster startup time for Quarkus Native. This difference has significant operational implications:

  • Faster container scaling
  • Better recovery from crashes
  • Improved behavior in spot/preemptible environments
  • Reduced cold start impact in serverless-style deployments

While startup time doesn't directly affect steady-state throughput, it dramatically impacts operational elasticity and cost optimization strategies.

Stress Test Results Analysis

The tests were conducted using wrk with different concurrency levels. Let's examine the results across each scenario.

Test 1: 10 Concurrent Connections

HTTP Throughput & Latency

Under 10 concurrent connections:

  • Spring Boot processed ~3.4× more requests per second (3939.24 vs 1168.78)
  • Spring achieved significantly higher HTTP throughput
  • Spring saturated the CPU (100%), while Quarkus peaked at ~71%

However, Quarkus showed lower average latency per request (6.86ms vs 12.44ms) despite processing fewer requests per second. This suggests:

  • Quarkus handles individual requests efficiently
  • Spring increases throughput at the cost of higher average latency
  • Spring likely increases internal concurrency to maximize CPU utilization

This reflects a classical trade-off: Spring → Higher throughput, Quarkus → Lower per-request latency

CPU Utilization

  • Spring fully saturated the allocated CPU (100%)
  • Quarkus peaked around ~70%

This means Spring extracted maximum processing capacity from the allocated core, while Quarkus left ~30% CPU headroom. If raw throughput is the goal, Spring utilized available compute resources more aggressively.

Memory Usage

The memory difference was significant:

  • Quarkus Native: ~40 MB
  • Spring Boot: ~256 MB

That's roughly 6× higher memory usage for Spring. From a container density perspective, Quarkus Native is substantially more memory-efficient—a difference particularly relevant in high-density container deployments or memory-constrained environments.

Kafka Consumption Behavior

In equal-volume scenarios:

  • Both frameworks inserted the same total number of messages
  • Quarkus drained the Kafka backlog ~45 seconds faster

Summary Under 10 Connections

Metric Spring Boot Quarkus Native
HTTP Throughput Higher (~3.4×) Lower
CPU Utilization 100% ~70%
Memory Usage Higher (256MB) Much Lower (40MB)
Average Latency Higher (12.44ms) Lower (6.86ms)
Kafka Drain Time Slower Faster

Test 2: 50 Concurrent Connections

HTTP Throughput & Latency

Under 50 concurrent connections:

  • Spring Boot processed more requests per second (7123 vs 6038)
  • The throughput gap narrowed compared to the 10-connection test
  • Both frameworks scaled significantly relative to low concurrency

However, Quarkus maintained significantly lower latency (~8.31ms vs 17.13ms). This suggests:

  • Quarkus continues to handle individual requests more efficiently
  • Spring increases throughput by pushing concurrency harder
  • Latency under load grows more aggressively in Spring

The same trade-off is reinforced: Spring → Higher throughput, Quarkus → Lower latency stability

CPU Utilization

  • Spring saturated CPU at 100%
  • Quarkus reached ~96.7%

At this concurrency level, both frameworks effectively utilized nearly the full CPU capacity. The previous CPU utilization gap largely disappeared, indicating that under higher concurrency, Quarkus is capable of fully utilizing allocated CPU resources.

The throughput difference here is no longer explained by unused CPU but more likely by:

  • Internal threading model differences
  • HTTP stack implementation
  • Serialization overhead
  • Framework-level concurrency strategies

Memory Usage

Memory efficiency remained highly differentiated:

  • Quarkus Native: ~58.8 MB
  • Spring Boot: ~256 MB

Spring maintained roughly 4–5× higher container memory usage. Heap usage showed Quarkus (native compatibility metrics) at ~38.4 MB versus Spring JVM heap at ~101 MB.

Kafka Consumption Behavior

At 50 connections:

  • Both frameworks consumed Kafka messages at approximately the same time
  • Total inserted messages were identical: 1,580,540 records each
  • However, Spring produced more total messages during the test (~1,706,765 vs ~1,448,099)

Summary Under 50 Connections

Metric Spring Boot Quarkus Native
HTTP Throughput Higher Slightly Lower
CPU Utilization 100% ~97%
Memory Usage Higher (256MB) Significantly Lower (59MB)
Average Latency Higher (17.13ms) Much Lower (8.31ms)
Kafka Consumption Time Equivalent Equivalent
Total Messages Produced Higher Lower

Test 3: 200 Concurrent Connections

HTTP Throughput & Latency

Under 200 concurrent connections:

  • Quarkus Native significantly outperformed Spring in throughput
  • Quarkus: ~9,942 requests/sec
  • Spring: ~7,340 requests/sec
  • This represents a ~35% higher HTTP throughput for Quarkus
  • Both frameworks reached full CPU saturation

At the same time, Quarkus maintained lower average latency (~24.47ms vs 33.18ms). This is an important shift from lower concurrency tests:

  • At high concurrency, Quarkus not only preserves latency efficiency but also overtakes Spring in raw throughput
  • Quarkus scales more efficiently under heavy parallel load
  • Spring's throughput advantage at low concurrency does not persist at higher concurrency
  • Native execution overhead remains stable even under CPU saturation

CPU Utilization

Both frameworks saturated CPU at 100%, with both limited by the same 1-core container constraint. This means throughput differences are not caused by CPU underutilization but by internal framework efficiency under load.

At this point, Quarkus converted CPU cycles into more HTTP throughput than Spring.

Memory Usage

Memory efficiency remained dramatically different:

  • Quarkus Native: ~71.5 MB
  • Spring Boot: ~256 MB

Even under heavy load, Quarkus consumed less than one-third of Spring's container memory. Spring memory remained flat at the container limit. Heap usage showed Quarkus compatibility metric at ~45.9 MB versus Spring JVM heap at ~101 MB.

Kafka Consumption Behavior

Under 200 connections:

  • Both frameworks inserted the same total number of records: 2,075,484 messages each
  • Quarkus produced significantly more total messages (~2,387,534 vs ~1,763,434)
  • Despite producing fewer messages, Spring drained Kafka 90 seconds faster

Summary Under 200 Connections

Metric Spring Boot Quarkus Native
HTTP Throughput Lower Highest (~35% more)
CPU Utilization 100% 100%
Memory Usage Higher (256MB) Significantly Lower (72MB)
Average Latency Higher (33.18ms) Lower (24.47ms)
Total Messages Produced Lower Higher
Kafka Drain Time Faster Slower

Test 4: Extreme Concurrency and Memory Limits

The final test could not be completed as planned. During execution, the Spring Boot container was terminated due to an out-of-memory (OOM) condition. Inspection of the container state revealed:

  • Exit code 137: Process was killed by the system (SIGKILL)
  • OOMKilled=true: Container exceeded its memory limit

The Docker memory limit for Spring was set to 256MB, which was insufficient under this load. This highlights a critical aspect of the comparison: under extreme concurrency, Spring Boot (running on the JVM) required more memory than the configured container limit allowed, leading to forced termination.

Meanwhile, Quarkus Native continued operating within its allocated memory constraints, demonstrating superior resilience under memory pressure.

Operational Implications

The benchmark revealed several important operational considerations:

Container Density and Resource Efficiency

Quarkus Native consistently demonstrated significantly lower memory usage across all concurrency levels. This translates directly to:

  • Higher container density per host
  • Lower infrastructure costs
  • Better utilization of memory-constrained environments
  • Reduced need for memory overprovisioning

Scalability Patterns

The frameworks exhibited different scaling behaviors:

  • Spring Boot: Excelled at low concurrency with aggressive CPU utilization but struggled at higher concurrency
  • Quarkus Native: Demonstrated more consistent performance across concurrency levels, eventually outperforming Spring at high load

Resilience Under Pressure

The OOM condition in the final test demonstrates a critical operational difference:

  • Spring Boot can hit hard memory limits under sustained load
  • Quarkus Native maintains stable memory usage even under extreme conditions

This has significant implications for:

  • Spot/preemptible instance usage
  • Auto-scaling scenarios
  • Environments with strict memory quotas

Startup Time and Elasticity

The 12× faster startup time of Quarkus Native enables:

  • Faster container scaling events
  • Quicker recovery from failures
  • Better performance in serverless architectures
  • Reduced cold start penalties

Framework vs Runtime Strategy

The most significant finding wasn't about which framework is faster, but how runtime strategy impacts behavior more than framework choice. The comparison between Spring Boot on JVM and Quarkus Native revealed fundamentally different performance characteristics:

  1. Memory efficiency: Native compilation consistently outperformed JVM execution
  2. Scaling behavior: Different concurrency levels favored different approaches
  3. Operational resilience: Native compilation demonstrated better memory stability
  4. Startup characteristics: Native compilation enabled dramatically faster initialization

This suggests that for applications with specific operational constraints, runtime strategy may be a more important consideration than framework selection.

Conclusion and Recommendations

The benchmark demonstrated that there's no universal winner between JVM and native execution approaches. The optimal choice depends on specific workload characteristics, infrastructure constraints, and operational objectives.

When to Consider Spring Boot on JVM

  • Applications with abundant memory resources
  • Scenarios where raw throughput at low concurrency is critical
  • Environments with mature JVM tuning expertise
  • Applications with complex reflection-heavy dependencies that don't GraalVM compatibility

When to Consider Quarkus Native

  • Memory-constrained environments or high-density deployments
  • Spot/preemptible instance usage where fast startup matters
  • Serverless or container-based architectures with cold start penalties
  • Scenarios requiring predictable memory usage under variable load
  • Applications with compatible GraalVM native image requirements

The decision should be guided by workload profile, infrastructure constraints, and operational goals—not by hype. For financial systems with aggressive cost optimization strategies and deployment on spot instances, the native compilation approach offers compelling advantages in resource efficiency, startup time, and operational resilience.

Further Exploration

For organizations considering this transition, the following resources provide valuable information:

Understanding these trade-offs enables more informed architectural decisions that align with specific operational requirements rather than following one-size-fits-all approaches.

Comments

Loading comments...