Image: Google Gemini

Quarkus vs. .NET: Beyond the Feelings, Into the Numbers

In my daily business, I am confronted with statements like "Java is old," "Java is slow," or "Java is inefficient," while .NET is often portrayed as the modern, "cool" alternative. These are often feelings, not numbers. But as engineers, we should rely on measurements.

Inspired by the work of Holly Cummins and Eric Deandrea, I decided to fork their benchmark repository to conduct a head-to-head comparison between Quarkus (Java) and .NET (C#). I wanted to see clear directions — not in a high-tech performance lab, which rules out external factors as much as possible, but in a way that satisfies professional curiosity.


The "Rich Buffet" of Choices

One of the main reasons I stick with Java and Quarkus is its deployment flexibility. I can use a single codebase to deploy as a CLI, a systemd service on Linux, a Windows Service, or in Podman/Kubernetes — either in JVM-mode or as a Native binary. Having Quarkus, this is even easier than with plain Java. It offers a rich ecosystem of well maintained extensions, does amazing optimizations to the runtime and provides an awesome developer experience in dev-mode. It is truly a "rich buffet" of everything a developer likes to choose from.

Core Observations & Measurement Summaries

1. Throughput vs. Memory: The Heavyweights

The results highlight a distinct trade-off between the two platforms:

  • Quarkus is the king of throughput. In my measurements, the requests per second (tps) it can handle are truly amazing, with some JVM configurations reaching over 18,600 tps.
  • Dotnet is the king of memory usage. In standard CLR mode, it consumes significantly less memory than Java in JVM mode. In memory-constrained runs, .NET held steady at 223 MiB RSS while plain Quarkus-JVM required 450 MiB.
  • The "Throughput Density" Equalizer: Measuring Requests per second / RSS (Resident Set Size) provides a fascinating perspective. Because Java's high memory usage can "destroy" its transaction advantage, Java and .NET often end up head-to-head on this efficiency metric.

2. The Impact of Heap Configuration

I compared two distinct scenarios: Run 9 (Memory-Constrained) using a 256 MiB heap and Serial GC, versus Run 10 (Fixed-Heap) using a 512 MiB heap and Parallel GC.

Runtime tps (256MB Heap) tps (512MB Heap) Δ%
quarkus-jvm 12,023 18,657 +55%
quarkus-leyden 10,594 17,074 +61%
dotnet-aspnet-ef 6,530 6,601 +1%

Plain JVM and Leyden were the big winners of the larger heap, seeing performance jumps of over 55%. Meanwhile, dotnet barely moved (+1%). It is important to note that tuning didn't have an effect on .NET here simply because it was always operating below the tuned values, making it the most predictable choice for memory-constrained environments that still require a dynamic runtime (CLR).

3. Project Leyden: The Startup Game Changer

Without Project Leyden, .NET wins the race in startup time (excluding native builds). Leyden is a real disruptor:

  • Startup Speed: It slashes Time-to-First-Response (TTFR) from ~2.6 seconds down to under 1 second.
  • The Throughput Trade-off: While Leyden improves startup, it consistently lowers peak throughput compared to plain JVM. However, it pulls ahead in "long-running" scenarios once GC pressure relaxes, making it a strong choice for realistic container budgets (~512 MiB).

4. Native Image: Mandrel vs. GraalVM CE

I compared Mandrel against GraalVM CE. Surprisingly, they aren't identical:

  • Size & Detection: Mandrel produces larger binary images, and the detection of fields and methods can differ despite using the same version.
  • Performance: Both are absolute winners for Serverless architectures, crushing JVM and .NET variants with a TTFR of approximately 100ms.

Detailed Measurement Reports

You can find the raw data and full logs of some of my 2026 testing runs here:

The Verdict: Which one to choose?

Scenario Recommendation
Serverless / Fast Startup Quarkus Native — Beats .NET across almost every metric if you don't need dynamic features.
Memory Constrained & Dynamic dotnet-aspnet-ef — The go-to when you need a CLR/JVM but have a very tight RAM budget.
Standard Container (~512m) quarkus-leyden — Great balance of startup and throughput.
Throughput-First quarkus-jvm — Still the undisputed king for raw requests per second.

Final Thoughts

I’m certainly not a "pro" on either platform, but I’ve always preferred following numbers over just following a gut feeling. I suspect that while my setup isn't a high-end performance lab, the results carry more weight than one might think. I noticed that while runs with fewer than 7 iterations can be a bit jumpy, the numbers seem to settle into a surprising consistency once you hit that 7-iteration mark - most probably due to statistical smoothing. It leads me to believe that these patterns are quite stable.


For the full raw data, check out the repository: itbh-at/quarkus-dotnet-benchmarks