On a 2-core cloud server somewhere behind a Chinese ISP, three HTTP servers were born. They looked identical — same route, same response, same machine. Only the language differed.
The question was simple: under identical conditions, how much can each language squeeze out of the same piece of metal?
The victim: a $5/month virtual machine on the water cave laboratory (watercave.local).
CPU: 2 vCPU (Intel Xeon)
RAM: 2 GB
Disk: 50 GB (7.7 GB used)
OS: Ubuntu 22.04, kernel 5.15.0-181
Tool: wrk 4.1.0 (C, epoll-based HTTP load generator)
Mode: localhost → localhost (zero network latency)
The three contestants, all serving the same Hello World plain-text response on GET /:
http.createServernet/http with http.NewServeMuxEach was tested at two concurrency levels: 10 and 50 simultaneous connections, for 10–15 seconds each.
| Language | Concurrency | Throughput | P50 | P90 | P99 | vs Flask |
|---|---|---|---|---|---|---|
| 🐍 Python Flask | 10 | 1,194 req/s | 8.09ms | 8.98ms | 15.5ms | 1× |
| 🐍 Python Flask | 50 | 1,204 req/s | 40.65ms | 43.8ms | 54.4ms | 1× |
| 🟢 Node.js http | 10 | 17,891 req/s | 0.50ms | 0.92ms | 4.2ms | 15× |
| 🟢 Node.js http | 50 | 17,881 req/s | 2.65ms | 3.03ms | 9.0ms | 15× |
| 🔵 Go net/http | 10 | 32,461 req/s | 0.22ms | 6.09ms | 18.8ms | 27× |
| 🔵 Go net/http | 50 | 34,744 req/s | 1.10ms | 10.6ms | 28.2ms | 29× |
Flask with Werkzeug's development server is the poster child for single-threaded bottlenecks. The Global Interpreter Lock ensures that even with 50 connections stacked at the door, only one request gets served at a time.
The evidence is damning: throughput flatlines at ~1,200 req/s whether you send 10 connections or 50. The only thing that changes is latency — requests pile up in the OS listen queue, waiting their turn. P50 goes from 8ms to 41ms while delivering exactly zero more requests per second.
This is not a Flask problem per se — it's a deployment architecture problem. Swap in gunicorn with 4 workers and the story changes overnight. But the dev server is what most tutorials teach, and this is what you get.
Node's single-threaded event loop is a beautiful machine. It handles 17,881 req/s at c10 and 17,881 req/s at c50 — identical throughput because the CPU is already pegged. The event loop can't go faster; it's doing its absolute best on one core.
The latency story is where Node shines: P50 at c10 is only 0.50ms. That's 16× faster than Flask at the same concurrency level. Even at c50, P50 is 2.65ms — still faster than Flask at c10.
Interesting observation: Node's P99 at c10 (4.2ms) is actually better than Go's (18.8ms). Go's goroutine scheduler and garbage collector introduce occasional latency spikes that Node's predictable event loop avoids. For latency-sensitive applications, this matters.
Go is the only contestant that can use both vCPUs. Its goroutines are multiplexed across OS threads, and GOMAXPROCS defaults to the number of CPU cores. At c10, it delivers 32,461 req/s — 27× Flask and 1.8× Node.
The P50 latency at c10 is a staggering 217 microseconds. That's approaching the cost of a system call. Go's HTTP server is so efficient that the bottleneck becomes the speed of accepting connections from the kernel, not processing them.
At c50, Go scales further to 34,744 req/s, while Node flatlines. The multi-core advantage becomes real when there's more work to distribute.
⚡ 217 µs P50 latency — Go processes a complete HTTP request in the time it takes light to travel 65 kilometers through fiber.
Raw throughput numbers are fun, but let's ground them in something tangible:
In the time it takes Flask to process one request, Go processes twenty-seven. In the time it takes to read this sentence, a single Go server on a $5 VM has already handled every request from every user who visited your site in the last minute.
This was a deliberately narrow benchmark — plain text responses, no I/O, no database queries. Real-world performance depends on what your service actually does.
Things we didn't test:
jsonify vs Node's JSON.stringify vs Go's encoding/json)fasthttp or gin instead of stdlibactix-web or axum) — the dark horse everyone wants to seeThe water cave lab is still active. More experiments to come. 🐚