In August I benchmarked Go against C# and did not enjoy the result. But I had a consolation prize ready:
Fine, Go is lean. But at least we beat Java.
I have written C# for years, and "modern .NET is faster than Java" is one of those facts you pick up without ever checking. Every talk and every "C# vs Java" video I have seen lands on the same side. I had honestly never heard anyone argue the opposite.
So I checked. I built the same invoicing API twice: once in Java 25 with Spring Boot and Hibernate, once in .NET 10 with Minimal API and EF Core. Same PostgreSQL, same Redis, same six CPU cores each. Then one 16-hour run and 44 million measured requests.
The short version:
- Speed: a tie. Both stayed fast up to about 4,800 requests per second.
- CPU: Java used half. .NET burned about twice the CPU time for every request.
- Memory: .NET used a quarter. 176 MB against Java's 656 MB.
- Money: Java was cheaper to run. A CPU core rents for 8 to 10 times the price of a gigabyte of RAM, even in the year RAM prices went crazy. Unless your services are small and sleepy. Then .NET wins.
I looked for the config mistake that explains it. I did not find one.
Why everybody "knows" .NET is faster
Mostly because of one table.
For over a decade, TechEmpower's Framework Benchmarks were the scoreboard everyone screenshotted. In the final round (Round 23, February 2025), the Fortunes test shows ASP.NET Core at about 610,000 requests per second and Spring at about 244,000 (the numbers people share). Two and a half times faster. Put that on a slide and the meeting is over.
The project shut down in March 2026, but screenshots are forever.
Here is the catch, which I went through in the last post: the rows at the top are hand-tuned entries with most of the framework ripped out. Nobody ships that code. It is a drag racer. It tells you nothing about what your family car burns on the way to work.
Last time, real code did not behave the way the scoreboard said it would. So I stopped trusting it for Java too, and measured.
The test
The app is called Ledgerline. It is a boring invoicing backend, on purpose. Look up an invoice. List a customer's invoices. Price a quote. Create an invoice with line items. PostgreSQL holds 1 million customers and 5 million invoices, and Redis caches the reads.
Both versions are written the way a normal team would write them.
| Java | C# | |
|---|---|---|
| Runtime | Java 25 (LTS) | .NET 10 (LTS) |
| Web | Spring Boot 4, virtual threads | ASP.NET Core Minimal API, async/await |
| Database access | Hibernate 7 | EF Core 10 |
| Garbage collector | the default (G1) | the default (Server GC) |
| Given to each | 6 CPU cores, 12 GB of RAM | 6 CPU cores, 12 GB of RAM |
The rule for both sides: if the project template gives it to you, it is in. If you would only add it to win a benchmark, it is out. I even removed four benchmark-flavoured tricks from my own .NET code before the run (a precompiled EF model, source-generated JSON, the slim app builder and a trimmed-down container image). Yes, I nerfed my own team.
Two .NET settings are not defaults, and both help .NET: DbContext pooling, which is common practice anyway, and automatic prepared statements in Npgsql, because Java's Postgres driver does that out of the box. Every other difference, on either side, is listed in the repo's FAIRNESS.md, including which side each difference hurts.
How the measuring works, in plain words:
- A load generator sends requests at a fixed pace. It does not slow down when the service does, so a struggling service cannot hide.
- I raise the pace step by step until the slowest 1% of requests take longer than 20 ms. (That "slowest 1%" number is called p99.) The traffic level where that happens is that stack's limit.
- Then I run both at their limits, back to back, ten times. CPU and memory are read from the Linux kernel's own counters, not from what the runtimes say about themselves.
Only one service runs at a time, and each gets a few minutes of warm-up traffic before anything is recorded.
The results
Speed: a tie
Java's limit was 4,799 requests per second. .NET's was 4,833. That is a 0.7% difference. A draw.
And neither was out of breath. At that point Java was using about 1.1 of its 6 cores and .NET about 2.3. So the limit comes from something in the shared plumbing, like the database connection pool, and not from the language. I did not chase down exactly what.
Which means the real question is not "who is faster". It is "what does each one spend to be that fast".
CPU: Java did it with half
At its limit Java spent 0.233 milliseconds of CPU per request. .NET spent 0.481. That is 2.1 times more. In the back-to-back runs Java used 51.6% less CPU, and the result barely wobbled between runs (95% interval: 51.3% to 51.8%).
This is not a number picked at a flattering spot. From 1,500 requests per second all the way up, the gap stays between 2.0x and 2.2x. At light traffic it is wider: 3x at 500 requests per second.
Memory: .NET did it with a quarter
Now the other side. At its limit .NET held 176 MB of memory. Java held 656 MB, which is 3.7 times more. At light traffic the gap is even bigger: 59 MB against 461 MB, almost 8x.
Java was not squeezed, by the way. It was allowed about 3 GB of heap and its heap never passed 328 MB. This is simply how much elbow room a JVM likes.
Two things I was sure about, and was wrong about
"Hibernate must be hammering the database." I had a reason to think so. Hibernate cannot batch inserts on this schema, so creating one invoice costs Java about 11.5 trips to the database where EF Core needs 6. I measured PostgreSQL's CPU separately to catch exactly this. Result: 0.101 ms per request when serving Java, 0.115 ms when serving .NET. Postgres did not care.
"The JVM takes forever to warm up." Before every measurement, each service gets traffic until it settles: steady response times, steady garbage collection. The fastest that can possibly happen is 183 seconds.
Java settled in 183 seconds, 53 times out of 53. .NET usually needed 213 seconds, and 11 times out of 63 it ran into the 300-second time limit. The JVM, famous for showing up late, was early to every single meeting.
To be clear, this is not start-up time. I did not measure how fast each app boots, so I am claiming nothing about that.
So why does .NET need twice the CPU?
Honest answer: I do not know. I did not profile it. I can only tell you what it is not.
- Not the database. See above.
- Not garbage collection pauses. .NET does collect a lot, about 26 times per second at its limit, because it keeps its heap tiny. That is how it stays at 176 MB. But those pauses add up to under 2% of the time. Even if all six cores were busy during every pause, that is about 5% of .NET's CPU. Not half.
- Not allocations. .NET actually allocated less per request: 68 KB against Java's 88 KB.
What is left is the request path itself: the web server, JSON, async, the database driver. Someone with a profiler and a free afternoon could probably name the top three. If that is you, the repo is open and I would love to know.
Same speed, different bill
So Java pays in RAM and .NET pays in CPU. Which one is cheaper depends on what RAM and CPU cost. And 2026 is a funny year to ask, because RAM prices went through the roof.
TrendForce, the people who track memory chip prices, forecast contract prices for ordinary DRAM up 55 to 60% in the first quarter, another 58 to 63% in the second, and 10 to 15% more this quarter. I fully expected that to flip my conclusion.
Then I opened the cloud price lists. These are the prices on October 20, 2026, US regions, pay-as-you-go:
| Cloud | 1 vCPU for an hour | 1 GB of RAM for an hour | CPU costs |
|---|---|---|---|
| AWS Fargate | $0.0405 | $0.0044 | 9x more |
| Azure Container Apps | $0.0864 | $0.0108 | 8x more |
| Google Cloud Run | $0.0864 | $0.0090 | 10x more |
(AWS bills per GB, the other two per GiB, which is 7% bigger. It does not change the picture.)
Nothing moved. Azure's own price feed dates its rate to June 2022. So far the memory crunch has landed on GPUs (AWS raised its reserved GPU prices twice this year), not on the plain CPU and RAM a web API runs on. That can change, so check the date on this post.
Now price what each service actually used at its limit, with the AWS numbers:
| Java | .NET | |
|---|---|---|
| CPU in use | 1.12 cores | 2.32 cores |
| Memory in use | 0.66 GB | 0.18 GB |
| CPU, per hour | $0.045 | $0.094 |
| RAM, per hour | $0.003 | $0.001 |
| Total, per hour | $0.048 | $0.095 |
Run that for a month and it is roughly $35 against $69, for one small service.
Java's extra 480 MB of RAM costs about a fifth of a cent per hour. .NET's extra 1.2 cores cost about five cents. So RAM would have to get about 23 times more expensive before the two bills meet. It has been a wild year for RAM. Not that wild.
Two honest footnotes on the money.
Clouds sell RAM in a bundle with CPU. You do not pay for what you use, you pay for the size you pick. Fargate will not sell you 2 vCPU with less than 4 GB of RAM, or 4 vCPU with less than 8 GB. Both services fit inside that with lots of room to spare, so on a platform like that Java's extra memory costs nothing extra at all.
Small, sleepy services are a different story. When traffic is low, the CPU bill shrinks toward zero and the memory bill stays. At 500 requests per second .NET fit in 59 MB where Java needed 461 MB. By my math the two bills cross at around 100 requests per second per instance. That is below anything I measured, so treat it as a rough guess. But if you run a hundred little services that mostly sit idle on a shared cluster, you can pack almost eight .NET services into the memory of one Java service. That is real money too.
The memory gap is partly a choice each runtime makes for you. Java's garbage collector takes the room it is given. .NET's keeps the heap as small as it can and collects 26 times a second to do it. Neither is wrong. They just bet on different things being expensive, and on today's price list the expensive thing is CPU.
The fine print
- One machine, one full run. A shorter rehearsal earlier the same day gave the same picture: about half the CPU for Java, 3.8x the memory.
- One kind of app. A database-and-cache business backend. Heavy math, streaming or gRPC could land somewhere else.
- Defaults only. I measured what the templates ship, not what a tuning expert could squeeze out of either side.
- The 4,800 limit is a bit optimistic. When I re-ran both at exactly that rate, the slowest 1% averaged 27 to 29 ms, not 20, for both. So the true limit is a little lower for both. The cost comparison does not care: the CPU gap is 2x or more at every rate I measured.
- Nine pairs, not ten. I planned ten back-to-back pairs. One .NET run never settled, so that pair was thrown out. My own rules said ten, so the report labels the verdict "directional".
- Versions. Java 25.0.4 and .NET 10.0.11, measured on August 27 and 28, 2026. Both are still the current LTS lines. .NET 11 lands in November and Java 27 came out in September. I have measured neither.
- No start-up times. Only how long each took to settle under traffic.
- One thing I found after the run. This machine has a Linux setting that packs work onto the CPU's "best" cores, and it was almost certainly on. In a later test on the same machine it caused stalls. This run shows no sign of them, but I cannot fully rule it out. That, and how I will find what really caps both at 4,800, is in NEXT-ROUND.md.
All of it, plus the raw data and checksums, is in the run's own report.
So, Java or C#?
Here is the thing about this rivalry: it is a family argument. C# was born in 2000 as Microsoft's answer to Java, and Java's creator James Gosling called it "sort of Java with reliability, productivity and security deleted". A quarter of a century later the two have copied each other's homework so many times that they ended up with nearly the same feature list: records, pattern matching, cheap concurrency, a long-term release every two years, a library for everything, and jobs on every continent.
So pick like this:
- Pick the one your team knows and can hire for. This beats everything below. A happy Java team will out-ship a grumpy C# team, and the other way around, by a lot more than 2x.
- Then look at your bill. Big busy services where CPU is the cost: this test says Java, at about half the compute. Lots of small quiet services where memory is the cost: this test says .NET, at a quarter of the memory or less.
- And fix your queries first. A missing index will cost you more than anything in this post.
Before this run I would have told you modern .NET is faster than Java and moved on. Now I would say they are the same speed, one of them is quietly cheaper to run, and it is not the one my career is built on.
That is twice in a row. I am still writing C# on Monday.
Do not take it from a scoreboard, and do not take it from me either. Measure your own service.
Comments I expect
"It's EF Core." The database did the same amount of work for both. And I tested EF Core against Dapper and raw ADO.NET in the last round: the ORM was not where the big cost was.
"Use Native AOT." Native AOT does not cover the reflection EF Core relies on, so it would be a different app.
"Give .NET a bigger heap." I did not test it. It would cost memory and probably save some CPU. But GC pauses were under 5% of .NET's CPU, so I would not expect it to close a 2x gap. (The small heap is .NET's default behaviour here. It is called DATAS.)
"Spring Boot is the slow Java. Try Quarkus." Mainstream against mainstream was the point. Leaner stacks exist on both sides.
"Can I run it myself?" Yes. Everything is in the repo: both services, the test runner and the raw results. A full run needs Linux and at least 15 CPU cores. A 20-minute smoke test runs on a laptop.
Round 1, Go vs C#: post. Round 2 repo with both services, the method, the fairness list and the raw data: github.com/HaikAsatryan/java-vs-dotnet-benchmark. Every measured number in this post comes from run full-20260827T174611-fedora, except the rehearsal line in the fine print. Cloud prices were checked on October 20, 2026.






Top comments (0)