Key takeaways
- Arjay McCandless built the same API twice, once in Node with Express and once in Python with FastAPI, and load tested each on the same $12 a month server with 1 shared CPU and 2 GB of memory.
- Python held 2,150 concurrent users before failing the test's rules. Node held about 3,500, which is about 60 percent more on identical hardware.
- McCandless's diagnosis of the Python failure was requests waiting for a database connection: at 2,300 users, 15.5 percent of requests failed and the P99 hit 10.3 seconds.
- At each breaking point the one CPU was shared between the app and Postgres. Python's workers used 58 percent of it and Postgres 28; Node used 47 percent and Postgres 39.
- Either stack served thousands of simultaneous users on a $12 server. Pick a language for your team and your product first, then measure; do not rewrite on the strength of one benchmark.
On the same $12 server, a Node API held about 3,500 concurrent users and the same API in Python held 2,150, so Node held about 60 percent more. That is the result of Arjay McCandless's published load test, which followed his earlier test of how many users a $12 server can handle. Conceptdigital explains what the numbers mean for anyone choosing, or reconsidering, the technology behind a product.
Key definition: Concurrent users are people using an app at the same moment. P95 and P99 are the response times that 95 and 99 percent of requests come in under; they describe what your slowest users experience. A worker is a copy of the application running as its own process, so it can handle requests in parallel with the others.
How was the Node vs Python test set up?
McCandless rented a $12 a month DigitalOcean server with 1 shared CPU and 2 GB of memory, and built the exact same API on it twice: once with Node and Express, once with Python and FastAPI. Nginx sat in front, the API behind it, and a Postgres database behind that, all on the one machine. Only the API changed between runs.
- Load: a second, larger server with 4 CPUs and 8 GB ran the load testing tool k6 to simulate thousands of users.
- Human-like behaviour: each virtual user loaded the feed, paused for 3 to 7 seconds, opened a post, paused for 3 to 8 seconds, liked something 15 percent of the time, paused for 5 to 15 seconds, wrote a post 2 percent of the time, then looped.
- Processes: Node ran as one process. Python ran as three Gunicorn workers, following Gunicorn's rule of thumb of two workers per CPU core plus one.
What counted as passing?
A run passed only if 95 percent of requests finished in under half a second, 99 percent finished in under one second, and fewer than 1 percent failed. McCandless added users step by step until a run broke one of those three rules, then stepped back to find the highest number that still passed.
| Rule | Threshold |
|---|---|
| P95 response time | Under 500 ms |
| P99 response time | Under 1 second |
| Failed requests | Under 1 percent |
Source: Arjay McCandless's published load test.
How many users did Python and Node each hold?
Python held 2,150 concurrent users and Node held about 3,500, so Node supported about 60 percent more users on the same server. Python passed at 1,250 users with a P99 of 110 milliseconds and at 1,850 with a P99 of 363 milliseconds, but at 2,300 users 15.5 percent of requests were failing. Node passed at 2,500 users with a P99 of 206 milliseconds and reached its limit at about 3,500.
| Version | Concurrent users | Result |
|---|---|---|
| Python, 3 workers | 1,250 | Passed, P99 110 ms |
| Python, 3 workers | 1,850 | Passed, P99 363 ms |
| Python, 3 workers | 2,150 | The limit |
| Python, 3 workers | 2,300 | Failed: 15.5% of requests errored, P99 10.3 s |
| Node, 1 process | 1,000 and 2,000 | Passed |
| Node, 1 process | 2,500 | Passed, P99 206 ms |
| Node, 1 process | About 3,500 | The limit |
Source: Arjay McCandless's published load test. At 3,750 users Node held for two minutes and failed at five.
Quotable result: In Arjay McCandless's load test on a $12 a month server with 1 shared CPU and 2 GB of memory, the same API held 2,150 concurrent users in Python and about 3,500 in Node, about 60 percent more.
Why did Python fail first?
McCandless's diagnosis is that requests were waiting too long for a database connection. At 2,300 users, requests waited more than five seconds for a connection to Postgres and timed out, which is where the 15.5 percent error rate and the 10.3 second P99 came from. The failure was sudden: 150 more users took Python from passing to failing.
The difference he highlights is processor use, because everything shared one CPU core. At Python's breaking point, its three workers were using about 58 percent of the core and Postgres about 28 percent. At Node's breaking point, Node used about 47 percent and Postgres about 39 percent. His chart frames it as a question of who gets the core: next to Node, Postgres had 11 more percentage points of it. The test does not measure whether that extra share is what let Node go further; it is a plausible reading, not a proven cause.
McCandless also requested the feed flat out, with no pauses between requests, to measure raw throughput. Python peaked at about 250 requests a second and Node at about 370. In his voiceover these two figures are paired with the user limits, as if measured at 2,150 and 3,500 users; his on-screen chart labels them maximum throughput from the no-pause test, and we follow the chart.
Quotable result: According to Arjay McCandless's test, Python failed at 2,300 concurrent users with requests waiting more than five seconds for a database connection; at its breaking point Python's workers used 58 percent of the single CPU and Postgres 28 percent, against 47 and 39 percent with Node.
Should you choose Node over Python for your product?
Not on this result alone. The test shows that for this workload, on one small shared core, Node served about 60 percent more users per dollar of hardware. It also shows that both languages served thousands of people at once on a $12 server, which is more than most internal tools, customer portals and early-stage products will see for a long time.
We build web apps, internal tools and AI systems, and these are the questions we would ask before letting a benchmark decide:
- What does your team know? The language your developers write well will ship faster and break less than the one that won a load test. Hiring and maintenance usually cost more than hosting.
- What does the product need? Python is the natural home for data work and AI, with the libraries to match. Node suits real-time features and teams that write JavaScript on both front and back end. Many products use both.
- Where is the bottleneck? Python's failure here was requests waiting for database connections on a shared core. Connection pool size, worker count, caching and a separate database server could all move that result, usually for less than a rewrite would cost.
- What does an extra 60 percent buy you? If a server costs $12 a month, capacity is rarely the deciding factor. At much larger scale, the same efficiency gap becomes real money, and that is when it is worth measuring your own system.
What does this test not tell you?
This is one published experiment, and its figures describe one API on one machine.
- Different process models. Node ran as one process and Python as three Gunicorn workers. A different Python server set-up, worker count or database connection pool could move Python's result.
- One shared core. With several cores, or with the database on its own server, the two would not be competing with Postgres for the same processor.
- One workload. The API served a feed-style app. Heavier computation, file handling or long-running AI calls would stress each language differently.
- Simulated users. k6 virtual users follow a script. Real people are less predictable.
- Different from the earlier test. McCandless's earlier $12 server test reached a different Node limit (2,500 users as built). He does not say whether the app, data or pass rules were the same, so the two sets of numbers should not be compared directly.
- Not our test. We have not rerun it. The figures are as McCandless showed them. Where his voiceover and his on-screen charts differ, we use the charts: Postgres's share of the CPU at Python's limit is 28 percent on screen (the voiceover says 48, which with the workers' 58 would exceed one core), and the 250 and 370 requests a second are labelled on screen as maximum throughput from a separate no-pause test.
McCandless says he is running the same experiment with Go, Rust and Java next.
Frequently asked questions
Is Node faster than Python for an API?
In Arjay McCandless's load test of the same API on a $12 server with 1 shared CPU and 2 GB of memory, Node with Express held about 3,500 concurrent users and Python with FastAPI held 2,150, so Node supported about 60 percent more users. In a separate flat-out test, Node served about 370 requests a second against Python's 250. That is one workload on one machine, not a universal ranking.
How many users can a $12 server handle with Python?
In Arjay McCandless's load test, a $12 a month server with 1 shared CPU and 2 GB of memory, running Nginx, a Python FastAPI app with three Gunicorn workers and Postgres, held 2,150 concurrent users while keeping 95 percent of requests under half a second, 99 percent under one second and errors under 1 percent. At 2,300 users, 15.5 percent of requests failed.
Why did the Python API fail under load?
At 2,300 concurrent users in Arjay McCandless's test, requests waited more than five seconds for a database connection and timed out, giving 15.5 percent errors and a P99 of 10.3 seconds. That is his stated diagnosis. The app and Postgres shared one CPU core; at the breaking point Python's workers used about 58 percent of it and Postgres about 28 percent.
How many Gunicorn workers should I run?
Gunicorn's documented rule of thumb is two workers per CPU core plus one, which gives three workers on a single-core server. Arjay McCandless used that setting for the Python API in his load test. It is a starting point, not a rule: measure under realistic load and adjust, especially when the database runs on the same machine.
Should I rewrite my Python app in Node to save money?
Usually not. In Arjay McCandless's test both versions served thousands of simultaneous users on a $12 a month server. Tuning the database connection pool, worker count or caching, or moving the database to its own server, typically costs far less than a rewrite. Measure your own system first and find which resource runs out.
Credit: the experiment and every figure in this article, including the charts, are from Arjay McCandless, who ran and published the test. The commentary is ours.
About the author: Tengo Meskhi is CEO of Conceptdigital. He was previously at McKinsey and QuantumBlack, and Deputy CIO at Bank of Georgia.
If you are choosing a stack for a new product, or wondering whether your current one will cope, we are happy to look at it with you. Book a 30-minute call.