The main issue, as already explained in the other answers by @Martijn Pieters and @Mikhail Gerasimov is that app.run
is blocking, so the line loop.run_forever()
is never called. You will need to manually set up and maintain a run loop on a separate thread.
Fortunately, with Flask 2.0, you don't need to create, run, and manage your own event loop anymore. You can define your route as async def
and directly await
on coroutines from your route functions.
https://flask.palletsprojects.com/en/2.0.x/async-await/
Using async and await
New in version 2.0.
Routes, error handlers, before request, after request, and teardown
functions can all be coroutine functions if Flask is installed with
the async
extra (pip install flask[async]
). It requires Python 3.7+
where contextvars.ContextVar
is available. This allows views to be
defined with async def
and use await
.
Flask will take care of creating the event loop on each request. All you have to do is define your coroutines and await
on them to finish:
https://flask.palletsprojects.com/en/2.0.x/async-await/#performance
Performance
Async functions require an event loop to run. Flask, as a WSGI
application, uses one worker to handle one request/response cycle.
When a request comes into an async view, Flask will start an event
loop in a thread, run the view function there, then return the result.
Each request still ties up one worker, even for async views. The
upside is that you can run async code within a view, for example to
make multiple concurrent database queries, HTTP requests to an
external API, etc. However, the number of requests your application
can handle at one time will remain the same.
Tweaking the original example from the question:
import asyncio
from flask import Flask, jsonify
async def send_notif(x: int):
print(f"Called coro with {x}")
await asyncio.sleep(1)
return {"x": x}
app = Flask(__name__)
@app.route("/")
async def notify():
futures = [send_notif(x) for x in range(5)]
results = await asyncio.gather(*futures)
response = list(results)
return jsonify(response)
# The recommended way now is to use `flask run`.
# See: https://flask.palletsprojects.com/en/2.0.x/cli/
# if __name__ == "__main__":
# app.run(debug=False, use_reloader=False)
$ time curl -s -XGET 'http://localhost:5000'
[{"x":0},{"x":1},{"x":2},{"x":3},{"x":4}]
real 0m1.016s
user 0m0.005s
sys 0m0.006s
Most common recipes using asyncio
can be applied the same way. The one thing to take note of is, as of Flask 2.0.1, we cannot use asyncio.create_task
to spawn background tasks:
https://flask.palletsprojects.com/en/2.0.x/async-await/#background-tasks
Async functions will run in an event loop until they complete, at which
stage the event loop will stop. This means any additional spawned
tasks that haven’t completed when the async function completes will be
cancelled. Therefore you cannot spawn background tasks, for example
via asyncio.create_task
.
If you wish to use background tasks it is best to use a task queue to
trigger background work, rather than spawn tasks in a view function.
Other than the limitation with create_task
, it should work for use-cases where you want to make async database queries or multiple calls to external APIs.