Improving Tornado Web Framework Performance: From Synchronous Blocking to Asynchronous asyncio
This article demonstrates how to build a basic Tornado server, measures its performance under blocking operations, and then progressively applies multithreading, thread‑pool executors, and finally asyncio to achieve dramatically higher QPS and lower response times.
Tornado is a Python web framework and asynchronous networking library originally developed by FriendFeed, capable of handling tens of thousands of concurrent connections using non‑blocking I/O.
The article first shows a minimal Tornado server (listening on port 8888) that returns a simple "hello world" response:
# -*- coding: utf-8 -*-
import tornado.ioloop
import tornado.web
class Index(tornado.web.RequestHandler):
def get(self):
self.write("hello world")
def make_app():
return tornado.web.Application([
(r"/", Index)
])
if __name__ == '__main__':
app = make_app()
app.listen(8888)
tornado.ioloop.IOLoop.current().start()When a blocking time.sleep(0.5) is added to simulate a slow operation, the single‑threaded server’s QPS drops dramatically (from ~1200 to 2) and response times increase from 503 ms to over 50 seconds.
The article explains that time.sleep blocks the entire Python interpreter, causing all concurrent requests to wait, which is why performance degrades.
Several solutions are explored:
Multithreading : each request spawns a new thread to run time.sleep . This avoids blocking the main thread but wastes resources and does not return the result of the background work.
class Index(tornado.web.RequestHandler):
def get(self):
name = self.get_argument('name', 'get')
t = threading.Thread(target=time.sleep, args=(0.5,))
t.start()
self.write("hello {}".format(name))Thread‑pool executor : a ThreadPoolExecutor is created and the blocking work is delegated to it using the @run_on_executor decorator. With only one worker thread the QPS remains low; increasing the pool size to 5 improves QPS to about 10 but still far from optimal.
from tornado.concurrent import run_on_executor
from concurrent.futures import ThreadPoolExecutor
class Index(tornado.web.RequestHandler):
executor = ThreadPoolExecutor(5)
@tornado.web.asynchronous
@tornado.gen.coroutine
def get(self):
name = self.get_argument('name', 'get')
rst = yield self.work(name)
self.write("hello {}".format(rst))
self.finish()
@run_on_executor
def work(self, name):
time.sleep(0.5)
return "{} world".format(name)Asyncio : replacing the blocking time.sleep with the non‑blocking asyncio.sleep and marking the handler methods as async allows the server to handle many more concurrent requests. Performance tests show QPS rising from 2 → 91 → 181 → 305 as the number of simulated users increases, until OS limits on file descriptors are reached.
import asyncio
class Index(tornado.web.RequestHandler):
async def get(self):
name = self.get_argument('name', 'get')
rst = await self.work(name)
self.write("hello {}".format(rst))
self.finish()
async def work(self, name):
await asyncio.sleep(0.5)
return "{} world".format(name)The final summary emphasizes that Tornado’s high performance is only realized when its asynchronous features are used correctly; merely switching to Tornado does not guarantee speed.
A table of common asynchronous libraries is provided, listing synchronous and asynchronous equivalents for MySQL, MongoDB, Redis, and HTTP.
360 Quality & Efficiency
360 Quality & Efficiency focuses on seamlessly integrating quality and efficiency in R&D, sharing 360’s internal best practices with industry peers to foster collaboration among Chinese enterprises and drive greater efficiency value.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.