Tornado
blocking code
asynchronous programming
Python
web frameworks

Running blocking code in Tornado

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Tornado is built around a non-blocking event loop. If you call slow blocking code directly inside a request handler, you stop that loop from serving other requests. The fix is not to pretend the code is asynchronous. The fix is to move the blocking work off the I/O loop, usually into an executor, and then await the result.

Why blocking code is a problem in Tornado

Blocking code includes operations such as:

  • slow file I/O
  • synchronous database drivers
  • network calls made through blocking libraries
  • CPU-heavy calculations

If you run those directly in a Tornado handler, the current process cannot keep servicing the rest of its event-driven work efficiently.

Bad example:

python
1import time
2import tornado.web
3
4
5class BadHandler(tornado.web.RequestHandler):
6    async def get(self):
7        time.sleep(5)  # blocks the event loop
8        self.write("done")

This looks simple, but it stalls the server thread for five seconds.

Use run_in_executor for blocking work

The standard Tornado solution is to push blocking work into an executor and await it.

python
1from concurrent.futures import ThreadPoolExecutor
2import time
3import tornado.ioloop
4import tornado.web
5
6
7executor = ThreadPoolExecutor(max_workers=4)
8
9
10def blocking_task():
11    time.sleep(2)
12    return "finished"
13
14
15class GoodHandler(tornado.web.RequestHandler):
16    async def get(self):
17        result = await tornado.ioloop.IOLoop.current().run_in_executor(
18            executor,
19            blocking_task
20        )
21        self.write(result)

Now the I/O loop stays responsive while the blocking function runs in a worker thread.

Choose thread pool or process pool based on the workload

For I/O-bound blocking code, a ThreadPoolExecutor is usually the right first choice.

For CPU-bound work, threads may still compete under the GIL, so a ProcessPoolExecutor can be a better fit:

python
1from concurrent.futures import ProcessPoolExecutor
2import tornado.ioloop
3import tornado.web
4
5
6cpu_pool = ProcessPoolExecutor(max_workers=2)
7
8
9def heavy_compute(n: int) -> int:
10    total = 0
11    for i in range(n):
12        total += i * i
13    return total
14
15
16class CpuHandler(tornado.web.RequestHandler):
17    async def get(self):
18        result = await tornado.ioloop.IOLoop.current().run_in_executor(
19            cpu_pool,
20            heavy_compute,
21            2_000_000
22        )
23        self.write(str(result))

The choice depends on what is actually blocking:

  • use threads for blocking I/O
  • use processes for heavy Python CPU work

Keep the handler async even though the work is blocking

A useful mental model is that Tornado is still async at the handler boundary. You are just awaiting a future that represents work running elsewhere.

That means you should still:

  • mark the handler method async
  • await the future
  • avoid touching Tornado request state from worker threads or processes

Do the slow work off-loop, then return to the handler to write the response.

Limit executor usage deliberately

Executors are not free. If you push too much work into them, you just move the bottleneck elsewhere. Set reasonable worker counts and watch queueing behavior.

If the blocking work is large, frequent, or business-critical, the better design may be to push it into a separate job system instead of tying request latency to executor throughput.

Common Pitfalls

The biggest mistake is calling a blocking library directly inside a Tornado handler and hoping the async def keyword somehow makes it non-blocking. It does not.

Another issue is using a thread pool for CPU-heavy work and then being surprised by weak parallelism because of the GIL.

Developers also sometimes perform response writes or request-object access inside the worker function. That should stay on the main Tornado flow, not inside executor threads.

Finally, an executor can mask architectural problems for a while, but it is not a substitute for proper background processing if the task is long-lived or resource-intensive.

Summary

  • Never run slow blocking code directly on Tornado's event loop thread.
  • Use run_in_executor and await the result from the handler.
  • Use thread pools for blocking I/O and process pools for CPU-heavy Python work.
  • Keep request and response handling on the Tornado side of the await boundary.
  • If the workload is large enough, consider a dedicated background job architecture instead of in-request execution.

Course illustration
Course illustration

All Rights Reserved.