Let’s explore how a typical synchronous framework handles requests.
When a request arrives, the web server (WSGI server) assigns it to a worker, usually a thread or a process, which handles the request from start to finish.
# a typical synchronous framework routing
@app.route('/hello')
def hello():
# start
return 'Hello, World!'
# finish
This works fine for short requests, where processing times are in milliseconds, and many requests can be handled with relatively few threads.
However, this model struggles with long-lived connections like WebSocket or SSE, as each thread remains occupied for the duration of the connection.
For example, if you have 5 worker threads and all are tied up with long-lived connections, the 6th request must wait until a thread is free.
This problem can be replicated on httpout with wait because httpout is also threaded by nature.
# hello.py (httpout's file-based routing)
# start
import asyncio
async def main():
# simulate a long-lived connection
await asyncio.sleep(10)
print('Done!')
wait(main())
# finish
But not if you use run instead of wait
:
# hello.py (httpout's file-based routing)
# start
import asyncio
async def main():
# simulate a long-lived connection
await asyncio.sleep(10)
print('Done!')
run(main())
print('OK')
# finish (worker thread), main() will still run (on the main thread)
# should print the 'OK' first
With run()
you can immediately leave the worker thread while it is running main()
.
This essentially switches the style from threaded to asynchronous to handle long-lived connections - which should explain why the hybrid async-sync in httpout is so powerful in this case.
Note that this does not mean you should use run
in all cases, simply use wait
if you want to wait.