How much faster does your server get when you add more cores? You’d assume it would scale linearly, both on performance on cost, but who cares about assumptions. I tested it.
I ran the same app in Go, Rust, Java, and Python on servers with 1, 2, 4, and 8 cores, and measured how many users each one could handle before it slowed down.
Technical Spec / Setup
I used four DigitalOcean Premium AMD droplets from the same family: 1 vCPU / 2 GB for $14 a month, 2 vCPU / 4 GB for $28, 4 vCPU / 8 GB for $56, and 8 vCPU / 16 GB for $112.
The app is a Medium clone (the RealWorld "Conduit" spec) with users, articles, feeds, comments, favorites, and follows. Nginx sits in front of it, and Postgres runs on the exact same box. There is no dedicated database server.
Every box started with the same 2.3 GB of data: 100,000 users, 500,000 articles, 5 million favorites, 2.5 million comments, and 2 million follows.
This time I didn't use k6. I wrote a custom load generator in Go and ran it from three separate servers so it could never be the bottleneck. Each virtual user browses like a real reader: opens the feed, reads an article, thinks for about 7 seconds, and sometimes comments or favorites something. About 5% of requests are writes.
To pass, a server had to hold the load for 5 straight minutes with p95 latency under 500ms, p99 under 1 second, and under 1% errors.
How each language uses more cores
Extra cores only help if your app can actually use them. So each language used its standard way of running on multiple cores. Every app got a database pool of 10 connections per core.
Go (net/http + pgx). Every connection gets its own goroutine, and Go's scheduler spreads them across one OS thread per core. You write plain blocking code and the runtime handles the rest.

Rust (axum + sqlx). #[tokio::main] starts Tokio's multi-threaded runtime with one worker thread per core. Each connection is an async task, and idle threads steal work from busy ones.

Java (Spring Boot with virtual threads). One line of config, spring.threads.virtual.enabled=true, puts every request on its own virtual thread. The JVM runs them on one carrier thread per core, and a virtual thread waiting on the database steps aside so another one can run.

Python (FastAPI). Because of the GIL, one Python process can only run Python on one core at a time. So Uvicorn runs one worker process per core, each with its own event loop and its own database pool.

8x the cores, 7x the users
More cores helped almost linearly. Going from 1 to 8 cores got every language roughly 7 times the users:
Go: 3,200 → 22,000 (6.9x)
Rust: 2,900 → 19,000 (6.6x)
Java: 2,900 → 21,000 (7.2x)
Python: 1,400 → 10,000 (7.1x)

The CPU was the limit every time. Connections, disk, RAM, and the load generators all had room to spare.
Why 1 → 2 cores was the weakest jump
For Go, Rust, and Java, the worst step was the very first one. Doubling from 1 to 2 cores only got 1.5x (Go) to 1.7x (Rust and Java) more users.
Part of that is the servers themselves. All four droplets report the exact same CPU, "DO-Premium-AMD". But when I benchmarked each one, the 2-core box's cores were 7% slower and the 4-core box's were 20% slower than the 1- and 8-core boxes. Same name, different speed.
The other part is that one core is unusually efficient. On the 1-core box, Go used 1.19 ms of CPU per request. On the bigger boxes it was 1.39–1.49 ms. With a single core there's no cross-core coordination for the app's runtime, Postgres's shared memory, or Nginx.

Adjust for the slower CPUs and Go, Rust, and Java kept 81–107% of perfect linear scaling.
Why Go, Rust, and Java tied
At 1, 2, and 4 cores, these three were within 10% of each other, which is about the run-to-run noise of this test. At 8 cores they spread out a bit: Go 22,000, Java 21,000, Rust 19,000. Rust landed 14% behind Go, not much beyond that noise.
The reason is where the CPU went. Postgres used about half the box (48–55%). The app only used 26–30%, and Nginx another 11–12%.

So even an infinitely fast app could at most roughly double the ceiling. A faster language can't fix a database that's eating half your CPU.
Also worth noting: Java's virtual threads kept up with Go's goroutines and Rust's Tokio at every size.
What about Python?
Python held about 40–50% as many users as the compiled languages: 1,400 on 1 core up to 10,000 on 8. Here the CPU split flips. The Python app used about half the box and Postgres about 30%.
But it scaled just as well as everything else: 7.1x from 1 to 8 cores.
One tip if you deploy Python: the classic Gunicorn advice is 2 × cores + 1 workers. I tested that too, and one worker per core won at every size, by 2% to 17%, while using less memory.
Great, but how many users per dollar can we support? First, a quick word from our sponsors:
Leave Granola and get up to 12 months free of Wispr Flow Notetaker + Dictation
If you have paid time left on an individual Granola plan, we'll match it with a Wispr Flow subscription that includes Notetaker and dictation, and add bonus time, up to 12 months total. Sign in or create a Wispr account and submit proof of your plan to check eligibility.
Cost: users per dollar
Each size up costs exactly double, and from 2 cores up, capacity roughly doubled too. So users per dollar stayed pretty flat. But the cheapest box won in every language.
Go held 229 users per dollar on the $14 box, and 175–196 on the bigger ones. Rust, Java, and Python showed the same pattern.

Full results
Max simultaneous users each language held for 5 minutes:

Language | 1 core ($14) | 2 cores ($28) | 4 cores ($56) | 8 cores ($112) | 8 vs 1 |
|---|---|---|---|---|---|
Go | 3,200 | 4,900 | 9,800 | 22,000 | 6.9x |
Rust | 2,900 | 4,900 | 10,000 | 19,000 | 6.6x |
Java | 2,900 | 5,000 | 9,300 | 21,000 | 7.2x |
Python | 1,400 | 2,500 | 4,100 | 10,000 | 7.1x |
So what does this actually mean?
For a typical CPU-bound web app, more cores scale almost linearly, whatever language you're using. If your box is maxed out on CPU, going bigger is a pretty safe bet.
The 1-core server is the best deal per dollar. But one core isn't practical for most real production workloads, so you'd probably still pay for the bigger machine.
If your database runs on the same box, it's probably your biggest CPU consumer. Before you rewrite your app in Rust, check where your CPU is actually going.
And don't trust the CPU name your cloud provider shows you. Same name, up to 20% difference in speed. Benchmark the box you actually get.
Have a good week!
Arjay.
P.S. If you want to chat with me directly or get advice on this stuff, the best way is through my private discord server included with Devmaxx. Try it free for 7 days at devmaxx.dev.
