Rust 10x Faster Than Spring? I Ran the Benchmarks. The Truth Is More Complicated.

When I was picking a runtime for a side project, I ran the latest TechEmpower benchmarks and community stress tests on the side. The numbers made me reconsider everything I thought I knew about Java performance.

Here’s the data first, then I’ll explain where it comes from.

ConfigurationThroughputP99 LatencyMemory
Spring Boot (JVM)4,200 req/s45ms280 MB
Spring Boot (Native)3,600 req/s38ms55 MB
Quarkus (Native)5,100 req/s28ms35 MB
Rust (Axum + Tokio)42,000 req/s3ms12 MB

Test conditions: simple JSON response + PostgreSQL query, 100 concurrent connections, 60-second sustained load.

Rust’s numbers are impressive — 10x the throughput of Spring Boot with 1/23rd the memory. But this isn’t a Rust evangelism piece. I want to talk about where these numbers actually come from, and when you don’t need Rust at all.

The GC Tax: What You Think Is Fast Is Actually Paying a Hidden Fee

The single biggest differentiator in performance is garbage collection. And most Java developers have no idea they’re paying this tax.

In Spring Boot’s JVM, GC Pauses Mean Your App Stops

Spring Boot runs on HotSpot JVM with G1GC as the default. Sounds sophisticated, but the mechanism is straightforward: your app creates objects on the heap, the heap fills up, and then everything stops — all threads freeze simultaneously — while garbage gets collected. Back to running.

That simultaneous freeze is the problem.

Under moderate load, the young generation fills up every 1–3 seconds, triggering a young GC pause of 5–15ms. Every few minutes a mixed collection runs, stretching pauses to 20–50ms. Under memory pressure, a Full GC freezes the entire JVM for 100–500ms.

You can’t see GC in your application code, but it faithfully shows up in tail latency metrics. Worse is GC jitter — an average 10ms pause sounds manageable, but the distribution curve has a long tail. One in every few thousand requests hits a 50ms+ pause. Your P99 looks great; your P99.9 is brutal.

If you’ve ever wondered why your “fast” Java service has random latency spikes, this is almost always the answer.

Quarkus Native Image: Trading Build-Time Work for Runtime Peace

When Quarkus is compiled to GraalVM Native Image, it uses Serial GC by default. A single-threaded stop-the-world collector, but because the entire heap is only 35–60MB, pause times drop to 1–3ms. And these pauses are predictable — no JVM-style jitter.

For extreme cases, you can use Epsilon GC — a no-op collector. It never reclaims memory; the process restarts when it hits the limit. Sounds crude, but it’s perfect for short-lived container workloads where Kubernetes handles the restart logic.

Rust: Memory Ownership Decided at Compile Time

Rust has no garbage collector. Not low-pause GC. No GC at all.

Memory management is resolved at compile time. When a variable goes out of scope, memory is freed immediately — no runtime overhead, no background threads scanning the heap, no “hold on.”

The result: no GC pauses, no GC CPU overhead, no jitter. Every request’s latency is determined purely by your code, not by when the GC decides to run. This is why Rust’s P99 is 3ms while Spring Boot’s is 45ms — not because Rust executes code faster, but because Rust never pauses.

Compilation Showdown: JIT Warmup vs Native Image vs LLVM

The second major differentiator is how your code becomes machine-executable instructions.

Spring Boot on JVM: Slow Start, Fast Finish

HotSpot’s C2 JIT compiler profiles your running code, identifies hot paths, and compiles them to optimized native machine code at runtime. After 60–90 seconds of warmup, Spring Boot’s throughput is genuinely good.

But there are two problems.

First, the cold start cliff. During the first 90 seconds after startup, code runs in interpreted mode with dramatic performance gaps:

  • First 30 seconds: P99 = 450ms
  • 30–60 seconds: P99 = 120ms (partially JIT-compiled)
  • After 90 seconds: P99 = 45ms (fully optimized)

The same service, the same person running it — 10x performance difference between cold start and warmed JVM. In containerized environments where pods scale up and down constantly, you’re frequently serving traffic on cold instances, exactly when you need performance most.

Second, startup time. JVM mode takes 2–3 seconds to start. For serverless functions and rolling deployments, that’s painful.

Quarkus + GraalVM Native Image: Set in Stone at Build Time

Native Image compiles your entire application to a standalone binary at build time. No JVM, no bytecode, no interpreter.

  • Startup: 0.03–0.06 seconds (vs 2–3 seconds on JVM)
  • No cold start cliff — peak performance from request #1
  • 5–8x less memory (no class metadata, no JIT compiler, no bytecode resident in RAM)

The trade-off: without JIT runtime optimization, peak throughput is typically 10–25% lower than a fully warmed HotSpot JVM. The JIT compiler can do speculative optimizations based on runtime profiling that AOT simply cannot.

That’s why Spring Boot Native (3,600 req/s) is slightly slower than Spring Boot JVM (4,200 req/s) — but this comparison only holds after the JVM is fully warmed up.

Rust + LLVM: Zero-Cost Abstractions

Rust compiles directly to machine code via the LLVM backend. Like Native Image, it’s AOT compiled, but goes further: no runtime substrate (GraalVM Native Image still carries 15–30MB of substrate), no GC at all, not even Serial GC. LLVM’s compile-time optimizations are extremely aggressive — code quality on par with C/C++.

There’s also zero-cost abstractions. Rust’s high-level constructs — iterators, async/await, generics — compile to the same machine code as hand-written loops. No runtime polymorphism overhead, no hidden dynamic dispatch cost.

This is why Rust delivers 42,000 req/s at 12MB memory. It’s not a “better Java” — it’s a fundamentally different computational model.

Where Does CPU Time Actually Go: Breaking Down Spring Boot vs Rust

Beyond the frameworks, here’s how CPU time is actually distributed.

Spring Boot (JVM) — 4,200 req/s

  • 42%: business logic and database queries
  • 18%: JSON serialization/deserialization (Jackson)
  • 15%: GC overhead (G1GC background threads and pauses)
  • 12%: JVM infrastructure (class loading, reflection, proxy generation)
  • 8%: HTTP stack (Tomcat/Netty overhead)
  • 5%: JIT compiler background threads

Rust (Axum + Tokio) — 42,000 req/s

  • 70%: business logic and database queries
  • 12%: JSON serialization (serde, zero-copy, compile-time generated)
  • 0%: GC (doesn’t exist)
  • 0%: runtime overhead (no VM, no substrate)
  • 15%: Tokio async runtime + Hyper HTTP
  • 3%: system calls

The pattern is clear: the closer you get to Spring JVM, the higher the percentage of CPU spent on “framework tax.” Spring Boot JVM spends 42% of CPU on business logic and 58% on framework overhead. Rust spends 70% on business logic and 30% on HTTP/async infrastructure. Throughput is 10x different, but the CPU is doing the same work — the difference is how much is wasted.

Memory: Why This Isn’t Just a Technical Metric, but an AWS Bill Line Item

Memory isn’t just a technical number — it translates directly to money.

Comparing on AWS ECS Fargate (us-east-1, 20 instances, 24/7):

  • Spring Boot JVM (0.5 vCPU, 1GB/instance): ~$665/month
  • Spring Boot Native (0.25 vCPU, 0.5GB/instance): ~$180/month
  • Quarkus Native (0.25 vCPU, 0.5GB/instance): ~$180/month
  • Rust (0.25 vCPU, 0.5GB/instance): ~$180/month

The real cost cliff is JVM → Native — a 73% cost reduction. Fargate’s minimum allocation is 0.25 vCPU/0.5GB, so Quarkus Native and Rust land in the same pricing tier.

Scale that across 10 services: $6,650/month vs $1,800/month. That’s $58,000/year saved, just by moving from JVM to Native Image.

The real message isn’t “Rust is cheaper than Quarkus.” It’s that the JVM’s memory and CPU overhead is a real line item on your AWS bill, and GraalVM Native Image eliminates most of it without leaving the Java ecosystem.

GraalVM Native Image: The Most Pragmatic Choice for Java Shops

If you’re a Java team looking at Rust’s numbers with envy, GraalVM Native Image is the most pragmatic middle ground.

What Native Image gives you:

  • 40x faster startup (2.5s → 60ms)
  • 5–8x less memory (280MB → 35–55MB)
  • No JIT warmup cliff — peak performance from the first request
  • Sub-2ms GC pauses with Serial GC
  • Single binary deployment, no JRE dependency

The honest trade-offs:

  • Build time: 3–7 minutes (vs 10–25 seconds for JVM) — hurts developer inner loop
  • Peak throughput: 10–25% lower than fully warmed JVM (no JIT speculative optimization)
  • Ecosystem gaps: some Java libraries use reflection in ways that break under AOT
  • Debugging limitations: no jstack, jmap, JFR, VisualVM — the entire JVM tooling ecosystem is gone
  • Closed-world assumption: no dynamic class loading, no runtime bytecode generation

Recommended use cases:

  • I/O-bound services (REST APIs, CRUD services, event consumers) — where startup speed, memory, and latency predictability matter more than peak CPU throughput, use Native Image.
  • Compute-heavy workloads (batch processing, stream processing, ML inference) — where the JIT compiler’s runtime optimization genuinely delivers 15–25% better throughput, stay on JVM.

I deployed a Quarkus + GraalVM Native Image REST API as an AWS Lambda function, served through CloudFront → API Gateway. Cold start: 500ms. Lambda memory: 1024MB. The same API on Spring Boot JVM Lambda needed 2048MB memory and still cold-started at 10–15 seconds. Quarkus Native cut memory in half and startup by 20–30x — the difference between a usable serverless API and one that makes users stare at a loading spinner. Monthly cost for the entire stack: under $50.

Rust: When Even Native Image Isn’t Enough

Let me be clear about where Rust makes sense and where it doesn’t.

Cloudflare’s edge proxy, Discord’s message system, AWS Lambda’s Firecracker VM, Linkerd’s service mesh proxy — all Rust, all chosen for the same reasons: P99 < 5ms or throughput > 20K req/s is a hard requirement.

But Rust has a real productivity gap: a senior Java developer ships a production REST API in 2–3 days. The same API takes 5–7 days for an experienced Rust developer, and 2–3 weeks if the team is learning Rust.

Rust’s compiler is famously strict. The borrow checker catches memory bugs at compile time — which is why Rust has zero memory safety CVEs — but the learning curve is steep. “Fighting the borrow checker” is a real phase every Rust newcomer goes through for 2–4 months.

For enterprise CRUD services, a 10x performance advantage rarely justifies a 2–3x productivity cost. For performance-critical infrastructure, the equation flips.

The Decision Matrix

After benchmarking and deploying all configurations in production, here’s my framework:

Most enterprise API services: Quarkus + GraalVM Native Image. Best balance of Java ecosystem familiarity, team productivity, and performance gains.

Spring-heavy teams: Spring Boot 3.x + GraalVM Native Image. Don’t migrate frameworks unless you have a compelling reason. Native Image closes most of the performance gap.

Performance-critical paths (API gateways, data ingestion pipelines, real-time event processors, P99 < 5ms is a hard requirement): Rust. Worth the investment for the right use case.

Compute-heavy workloads (batch processing, stream processing, ML inference): Keep Spring Boot or Quarkus on JVM. The JIT compiler genuinely helps here.

The hybrid approach that works: API gateway in Rust, core business services in Quarkus/Spring Native, batch jobs on JVM. Use the right tool for each service’s actual requirements.

Data Summary

The performance gap is real:

MetricJVMNativeRust
Startup2,500ms25ms3ms
Memory280MB35MB12MB
Throughput4,200 req/s5,100 req/s42,000 req/s
P99 Latency45ms28ms3ms
GC Pauses5–50ms1–3ms0ms

If you’re running Java today, GraalVM Native Image with Quarkus is the highest ROI performance move you can make. Half the memory, 40x faster startup, sub-2ms GC pauses. Your team ships on day one — no new language to learn.

Save Rust for the 5% of services where P99 < 5ms is a hard business requirement. For everything else, Native Image closes the gap enough.

The JVM isn’t dying. But running it without Native Image in 2026 means leaving money on the table.


FAQ

Q: Quarkus Native Image vs Spring Boot Native Image — which is better?

If your team is already on Spring, the migration cost may outweigh the performance gains. Spring Boot 3.x’s Native Image support is mature, and the performance gap with Quarkus Native is small. But if you’re starting fresh, Quarkus’s cloud-native design gives it an edge in Native Image scenarios.

Q: I have a high-traffic API but my whole team is Java. How do I transition?

Don’t rewrite everything at once. Deploy one non-critical service on Quarkus Native Image first. Get familiar with the build pipeline and ecosystem compatibility. Then migrate gradually. A hybrid architecture is also a valid choice — Rust for the most critical front-end gateway, Java services for business logic.

Q: Can GC jitter be mitigated without going Native?

Yes. ZGC and Shenandoah are low-pause collectors that can reduce pauses to sub-millisecond levels. But they still have CPU overhead and jitter, and memory usage is higher than Native Image. This is the best option inside the JVM, but it still has a fundamental gap compared to true no-GC.