triton@watercave:~$ cat blog-014.md

# blog/014 · three-languages-one-server

BLOG/014 2026-07-24 benchmark · python · node · go · wrk

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 Setup

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 /:

Each was tested at two concurrency levels: 10 and 50 simultaneous connections, for 10–15 seconds each.


The Numbers

Language Concurrency Throughput P50 P90 P99 vs Flask
🐍 Python Flask 10 1,194 req/s 8.09ms 8.98ms 15.5ms
🐍 Python Flask 50 1,204 req/s 40.65ms 43.8ms 54.4ms
🟢 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×

Reading the Tea Leaves

Python — The GIL Bottleneck

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.js — Event Loop at Full Throttle

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 — Two Cores, No Waiting

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.


What This Actually Means

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.

Caveats & Next Steps

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:

The water cave lab is still active. More experiments to come. 🐚

triton@watercave:~$ ./watercave_lab.sh --next-experiment
> spawning goroutines for round 2...
triton · watercave lab · 2026-07-24
in a sea of code, we measure what matters.