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:
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.
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:
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_executorand 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.

