Blocking vs Non-blocking: Node vs Bottle

Node.js boasts real-time performance. Of course, depending on hardware, it may show some level od delay in responses, but it is usually able to handle requests as soon as they come in. I have built two simple hello world apps, one running on top of Node.js, and one running on top of Bottle/Bjoern. Bjoern is a non-blocking WSGI server written in C, and it should perform very well on its own, but Python is blocking, so we want to see how it stacks up against a pureluy non-blocking app written on Node.js.

Concept

We will use two applications, one written on Node.js, one written on Bottle/Bjoern stack. Both applications will return a ‘Hello World’ after a 1 second delay.

If everything works fine, the apps should be able to serve an arbitrary number of parallel requests in about one second total.

Bottle/Bjoern

Up first, Bottle/Bjoern. Bottle is version 0.9.6 (stable), and Bjoern is version 1.2.0 (stable). The stack runs on top of Python 2.7.2. The code looks like this:

from bottle import run, route
from time import sleep

@route('/')
def hello():
    sleep(1)
    return 'Hello world'

if __name__ == '__main__':
    run(server='bjoern')

Node.js

I won’t introduce you to Node itself this time. You can read one of my previous posts for that. Node is currently version 0.4.10. The test application looks like this:

var http = require('http');
http.createServer(function (req, res) {
    setInterval(function() {
      res.writeHead(200, {'Content-Type': 'text/plain'});
      res.end('Hello World\n');
    }, 1000);
}).listen(3000, "127.0.0.1");
console.log('Server running at http://127.0.0.1:3000/');

Before jumping to conclusion about syntax, and other aesthetical issues, you should be aware that Bottle is a web framework, whereas Node is a networked application framework. They have different priorities and this reflects itself in the API style used for our example code.

Testing tool

For this test, we are using ab utility. If you don’t have one on your system, it’s part of apache package. On some distros, it may be part of some apache-related package, I’m not sure. On Arch Linux, you should just install apache package, and you’ll have the tool.

Let’s get the show on the road

First, let’s start Bottle/Bjoern combo. I’ve put the test app inside a virtualenv.

So, we start the server and app with:

(bottlehello) $ python helloworld.py
Bottle server starting up (using BjoernServer())...
Listening on http://127.0.0.1:8080/
Use Ctrl-C to quit.

We will test with 10, 1000, and 1000 concurrent requests launched all at once. If X is the number of requests, the command is:

ab -c X -n X http://127.0.0.1:8080/

At X=10, Bottle/Bjoern show appaling performance (average ms / request):

10019.282 [ms] (mean)

At X=100, Bottle/Bjoern fails, so we didn’t even attempt X=1000.

Let’s move on to Node.js.

$ node helloworld.js
Server running at http://127.0.0.1:3000/

Using the same ab command as before but with port 3000, we get the following results for X=10, X=100, X=1000 respectively:

1011.443 [ms] (mean)
1029.896 [ms] (mean)
2148.086 [ms] (mean)

As you can see, at 1000 requests, it took 2 seconds for Node.js to handle the requests on average, but it did not fail. (It also ran at 1.5s/req on some earlier trials.)

Bottle/Cherrypy

To do Bottle some justice, we’ll try the test again using Cherrypy server. We will modify the run() line like so:

run(server='cherrypy')

And install Cherripy, obviously. Unlike single-threaded bjoern, Cherrypy is multithreaded, so it should be able to take advantage of all 8 cores on my developer machine.

And the test results for X=10, X=100, and X=1000 look like:

1325.132 [ms] (mean)

…well, it fails at 51 requests.

Bottle/Tornado

Just to be on the safe side, I’ve also tested with Tornado, and got similar results as in Bjoern’s case: 10s request time with 10 concurrent requests, and failed attempts to test X=100 and X=1000.

Conclusion

A single blocking call like time.sleep can make all the difference between scalability and failure. Now, you might argue that it is possible to make the above work with Bjoern/Python, and you are, indeed correct. If you replace the blocking time.sleep call with something non-blocking. This demo was meant to demonstrate the power of non-blocking, though, and I think the demo is successful at that.