One more week of stirring things up. Today’s topic is going to ruffle some feathers: Python versus NodeJS.

First things first: I write Python and I am not a NodeJS fan, but I will try to be objective. If the tests say NodeJS performs better, I will take it like a man and get on with my life.

And a heads up before we get into it: this week the Python courses at Securízame start, in Madrid. From scratch to advanced, and I teach the advanced part. All the details are at cursos.securizame.com/python-avanzado.

What this post is about

I am going to run a few load tests to see which of the two languages gives you more power for web development. Performance only.

I am not getting into which one is easier to program, paradigms or anything like that. Most of that is subjective and it is not what I am after here.

What we are comparing

Two things:

  • NodeJS with the Express module.
  • Python with asyncio and the aiohttp module.

On the Python side I use asyncio and aiohttp, and on the NodeJS side I add no extra module for asynchrony. Why? Because NodeJS is event driven from birth, the same way it is built for web services. It is part of its DNA.

Python, in its normal mode of operation, is not event driven. To make the comparison fairer I use asyncio, the new module in Python 3.4 that brings exactly that, and aiohttp, which makes it easy to build HTTP servers on top of asyncio.

Note: there are other Python libraries that give you asynchrony. I am going with asyncio because it is the one that aims to be the language’s reference point these days.

How the tests work

Very simple.

Site under test

A small web server that returns an HTML page.

What we measure

  • How many requests per second each one handles.
  • Total time to process X requests.
  • How many concurrent clients each one can take.

Code used

These are the sources for the tests.

Python

import asyncio

from aiohttp import web


@asyncio.coroutine
def home(request):
    return web.Response(body=b"""<!DOCTYPE html>
<html lang="es">
<head>
  <title>Hello world!</title>
</head>
<body>
<p>P&aacute;gina de prueba</p>
</body>
</html>""")


@asyncio.coroutine
def json_path(request):
    return web.json_response({"test": 1, "data": "hello world" })


def main():
    app = web.Application()
    app.router.add_route('GET', '/', home)
    app.router.add_route('GET', '/json', json_path)

    web.run_app(app, port=8081, backlog=5000)

if __name__ == '__main__':
    main()

NodeJS

const express = require('express');

// Constants
const PORT = 8080;

// App
const app = express();

// End-points
app.get('/', function (req, res) {
  res.send(`<!DOCTYPE html>
<html lang="es">
<head>
  <title>Hello world!</title>
</head>
<body>
<p>Página de prueba</p>
</body>
</html>`);
});


app.get('/json', function (req, res) {
  res.send(JSON.stringify({ test: 1, data: "hello world" }));
});

// Start server
app.listen(PORT, backlog=5000);

console.log('Running on http://localhost:' + PORT);

Note: for the tests to run properly you need to tweak the kernel parameters of your system. The defaults fall short and you have to raise them to handle more connections than the ones configured out of the box. The commands are here:

http://b.oldhu.com/2012/07/19/increase-tcp-max-connections-on-mac-os-x/

Results

Time to process requests

Command used: time ab -n NN http://127.0.0.1:PORT/

Where NN is the total number of requests.

Total time to process the requests

Requests NodeJS Python
2000 0.99 sec 2.02 sec
5000 2.57 sec 5.49 sec
10000 4.81 sec 12.85 sec

Requests per second

Requests NodeJS Python
2000 2072 req/sec 1005 req/sec
5000 1971 req/sec 915 req/sec
10000 2084 req/sec 779 req/sec

Time to serve X clients

Command used: time ab -c CC -n 10000 http://127.0.0.1:PORT/

Where CC is the number of concurrent clients.

Important note 1

In the aiohttp library, the web package ships with a limit of 128 concurrent connections. I do not know whether it is on purpose or an oversight.

The tests were run with that limit and also with a patch I prepared for the library. The patch is two lines of code, I sent it to the author and you can see it here:

https://github.com/KeepSafe/aiohttp/pull/892

Important note 2

NodeJS ships configured with 512 concurrent connections by default. To take more you have to change the backlog value when calling the listen function, like this:

app.listen(PORT, backlog=5000);

The official documentation is at:

https://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback

Run with the default configuration

That is: without the Python patch and without touching the NodeJS backlog.

Concurrent clients NodeJS Python
100 3.18 sec 9.82 sec
250 3.10 sec -
500 10.75 sec -
1000 - -

Run with the improved configuration

That is: with the Python patch and with the NodeJS backlog set to 5000.

Concurrent clients NodeJS Python
100 3.18 sec 9.82 sec
250 3.10 sec 10.581 sec
500 8.27 sec 9.90 sec
1000 2.89 sec 9.77 sec
4000 - 11.56 sec

Cells with “-“ mean the server refuses connections and cannot take that load.

Conclusions

Well, the numbers are what they are. Judge for yourself. I take two things away from this.

NodeJS performs quite a bit better, in general.

And the curious part: with 4000 concurrent clients NodeJS cannot serve them, and Python, at its own slow and steady pace, can.

I am no NodeJS expert. If anyone knows more than I do (it does not take much) and thinks the code is wrong or not quite right, please say so in the comments.

Bye!