Tornado is a fantastic Python web framework and asynchronous networking library which sadly doesn't get enough attention in the Python world. However I've found the framework to be an amazing choice for some of my requirements and I'm going to share some of my experiences with you.
Why Use Tornado
I'm a developer who works on his own personal projects in free time. I often have multiple small ideas and I want to try them out and put them in front of the world as a functional product. However I quickly found out after I had a few projects running that the VPS I was renting wasn't going to be able to handle all my projects. I had a couple of projects each running in their Docker container with a Django instance, a Celery worker, a Celery-beats daemon and the database servers. All this was quite a memory hog for me and I just wanted to be able to run more number of projects in the limited resources of my VPS. This is when Tornado started looking like a possible option for me as one of my apps was also extensively using websockets and Tornado is quite memory-efficient when it comes to keeping many long-running connections alive.
Celery used to be one of my favourite tools in web development as I have a penchant for ending up with many periodic tasks connected to the models involved in the web app. However during my analysis of memory-hogging processed I found that Celery was one of the major culprits. I was also intrigued by Tornado's delayed callbacks and periodic task features.
Finally I decided to give Tornado a go and also replace Celery with Tornado's delayed callbacks and my own mini library for handling scheduled tasks with support for persistency. Not only this lets you avoid running Celery workers in your stack, it lets you have a complete task scheduling service running in the same process as your web app if you so desire, which can be quite valuable when your app is running in a context where memory is scarce.
For the database I decided to switch from PostgreSQL to MongoDB because I felt it better suited for my new-found approach of extremely rapid prototyping and releasing updates. But that's a discussion for another day. However this approach can work equally well (or better) in the case of an RDBMS as well.
A few nice things about Tornado
One great thing I liked when switching to Tornado was that it immediately made me feel more in control of the somewhat lower levels of how an HTTP server behaves. Don't get me wrong, you can control these behaviours in frameworks like Django, Flask and Starlette as well, but generally these functionalities are somewhat hidden away as middlewares or there's no easy and direct way to change the lower level behaviours of a set of routes.
To take an example, if you had to allow CORS for a set of your endpoints in a language like Django or Flask, perhaps the most common approach is to install a corresponding package for the framework and add it to your application. So in Django you would install the django-cors-headers package and then add it to your installed apps, add it to your middlewares and then change the CORS policy in your setting. It works and solves your problem, but it hides away the inner details of its working and also makes your stack a bit heavier.
However the approach that Tornado takes is a bit different and in practice helps you be more in control of the underlying network behaviour. So what do you do when you want to allow CORS for some of your endpoints in Tornado?
You extend the RequestHandler class and override its
set_default_headers method to set the headers required for allowing CORS for the given request. It turns out that allowing CORS requires only modifying the value of one key in your HTTP header and can be done in just one line of Python. So no need to install a package and change your app configurations in different places to enable CORS, and you also get a much better understanding of what's happening behind the scene and can use this lower level of control to make your app more efficient or simpler.
class MyHttpRequestHandler(tornado.web.RequestHandler): def set_default_headers(self): self.set_header("Content-Type", 'text/html') # allow CORS requests self.set_header("Access-Control-Allow-Origin", "*") self.set_header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept") self.set_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
That's it for this blog post, I'll be talking about my experiences using Tornado as a lightweight replacement of Celery for task scheduling in my next post. If you want to be notified when there's new content up on this blog then do subscribe to the mailing list below!