The Seductive Promise of Async Speed
Sanic was built from the ground up to be fast, using Python's modern async/await capabilities to handle a massive number of concurrent connections without breaking a sweat. Unlike traditional synchronous frameworks that handle one request at a time per
worker, Sanic uses an event loop to juggle many tasks at once. When one request is waiting for a database query or an external API call to return, Sanic doesn't just sit there—it moves on to serve other requests. This non-blocking I/O is the secret sauce behind its benchmark-topping speed, making it a go-to choice for high-throughput APIs, microservices, and real-time applications where every millisecond counts. The entire philosophy is built on keeping this event loop running freely.
The Trap: A Blocking Call in an Async World
The performance trap is dangerously simple: making a synchronous, blocking call inside an asynchronous route handler. This could be using a traditional database library, making a network request with the popular `requests` library, or even performing a slow file-system operation with standard Python functions. To a developer new to the async paradigm, it looks like normal code. But in Sanic, it's a performance disaster. When you make a blocking call, you freeze the entire event loop for that worker process. All the concurrent requests that worker was expertly juggling come to a dead stop, waiting for that single synchronous operation to finish. It’s like closing all lanes of a superhighway to let one car make a U-turn.
Why It Negates Sanic's Core Advantage
The impact is catastrophic for throughput. An async server's efficiency comes from its ability to switch contexts during I/O waits. A blocking call robs it of this superpower. A single handler that takes 200 milliseconds to complete a synchronous database query will prevent that worker from processing any other requests for that entire duration. If you have four workers and four concurrent requests hit blocking code, your entire application becomes unresponsive until they complete. Suddenly, your high-speed async framework is performing worse than a properly configured synchronous one. You're using a Ferrari to haul furniture—it works, but you're missing the entire point and doing it inefficiently. The official Sanic documentation explicitly warns against this common mistake.
The Right Way: Offloading to an Executor
So how do you use a synchronous library when an async alternative isn't available? You don't just drop it into your async function. The correct solution is to run the blocking code in a separate thread pool managed by the event loop itself, so it doesn't halt the main thread. Sanic, through asyncio, provides a clean way to do this: `loop.run_in_executor()`. By wrapping your blocking function call in `run_in_executor`, you're telling the event loop, "Hey, this next part is going to take a while and will block. Please run it in the background in a managed thread and let me know when it's done. In the meantime, I'll get back to handling other requests." This approach keeps the main event loop free to do what it does best: manage I/O and serve requests with minimal delay.
Auditing Your Code for Hidden Blockers
Finding these hidden traps in your codebase is crucial. Start by searching for any standard I/O-bound libraries being used directly within `async def` handlers. Look for imports of `requests`, standard database drivers (that don't have an `aio` equivalent), or heavy file processing. Every time you find one, ask if it's blocking. If it is, it's a prime candidate for being wrapped in `run_in_executor`. While there's a small overhead to spinning up a task in the executor, it is negligible compared to the performance cost of blocking the event loop. Thinking asynchronously isn't just about adding the `async` and `await` keywords; it's about ensuring the entire call stack remains non-blocking.













