Async Views & ASGI

Reviewed & published by Brayan K

An async view is a Flask route defined with async def so you can await I/O inside it, letting several slow waits overlap instead of running one after another.

Part of the free Flask course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll write async views, run concurrent I/O with asyncio.gather, await an async client, and learn the WSGI limits and the ASGI servers that lift them.

What You'll Learn in This Lesson

1 Your First async def View

Since Flask 2.0 a view can be async def. Inside it you can await coroutines, and Flask runs the view on an event loop for you — the rest of your code (routing, the test client, returning JSON) is unchanged. The only setup is installing the async extra, which pulls in asgiref.

The runnable example defines an async view that awaits a coroutine and returns JSON. The test client calls it exactly like any other route.

# app.py — an async def view function (Flask 2+)
# (install support with: pip install "flask[async]")
import asyncio
from flask import Flask, jsonify

app = Flask(__name__)

@app.route("/async-hello")
async def async_hello():
    # 'await' inside a view: Flask runs it on an event loop for you
    await asyncio.sleep(0)          # stands in for awaiting I/O
    return jsonify(message="hello from an async view")

# The test client calls the async view exactly like a normal one:
with app.test_client() as client:
    r = client.get("/async-hello")
    print(r.status_code)            # 200
    print(r.get_json())             # {'message': 'hello from an async view'}

# app.run() would serve this forever, so it goes last and stays commented
# out — the test client above is how Flask itself exercises routes:
#     if __name__ == "__main__":
#         app.run(debug=True)

# ✅ Expected output:
# 200
# {'message': 'hello from an async view'}

The async view returns 200 with its JSON just like a sync view — from the caller's side nothing looks different. The power comes from what you can await inside.

2 Where Async Wins: Concurrent I/O

The payoff is overlapping waits. If a view needs three slow API calls, doing them sequentially takes the sum of their times. With asyncio.gather they run concurrently, so the total is roughly the slowest single call — not all of them added up.

The runnable example fires three awaitable "fetches" and times them. Because gather overlaps the waits, the elapsed time is close to one delay, not three — the code proves it with a concurrent flag.

# Why async helps: run several awaitable I/O calls concurrently
import asyncio, time
from flask import Flask, jsonify

app = Flask(__name__)

async def fetch(name, delay):
    # pretend this awaits a slow network call
    await asyncio.sleep(delay)
    return f"{name} done"

@app.route("/dashboard")
async def dashboard():
    start = time.perf_counter()
    # gather runs all three concurrently, not one after another
    results = await asyncio.gather(
        fetch("users", 0.05),
        fetch("orders", 0.05),
        fetch("stats", 0.05),
    )
    elapsed = time.perf_counter() - start
    # Concurrent, this is ~0.05s; one-after-another it would be ~0.15s. The
    # 0.12 threshold sits between the two: measured over 40 runs the
    # concurrent time was 0.0503-0.0504s, so the answer is not a close call.
    return jsonify(results=results, concurrent=elapsed < 0.12)

with app.test_client() as client:
    data = client.get("/dashboard").get_json()
    print(data["results"])                            # ['users done', 'orders done', 'stats done']
    print("ran concurrently:", data["concurrent"])    # ran concurrently: True

# ✅ Expected output:
# ['users done', 'orders done', 'stats done']
# ran concurrently: True

Three 50 ms waits finish in about 50 ms, not 150 ms, because they overlapped. That is the entire reason to write an async view.

3 Awaiting a Client & the WSGI Limit

In real code you await an async-aware library like httpx.AsyncClient. The runnable example uses a small stub that mimics its async with / await client.get(...) shape so the demo runs offline, but the structure is exactly what you'd write with httpx.

# await-ing an async client (here a stub mimicking httpx.AsyncClient)
import asyncio
from flask import Flask, jsonify

app = Flask(__name__)

class FakeAsyncClient:
    # stands in for httpx.AsyncClient so the demo runs offline
    async def get(self, url):
        await asyncio.sleep(0)             # would be a real network await
        return {"url": url, "status": 200}

    async def __aenter__(self): return self
    async def __aexit__(self, *exc): return False

@app.route("/proxy")
async def proxy():
    async with FakeAsyncClient() as client:
        resp = await client.get("https://api.example.com/data")
    return jsonify(fetched=resp["url"], status=resp["status"])

with app.test_client() as c:
    print(c.get("/proxy").get_json())
    # {'fetched': 'https://api.example.com/data', 'status': 200}

# ✅ Expected output:
# {'fetched': 'https://api.example.com/data', 'status': 200}

🔬 Async Cheat Sheet

You want to...Use
Define an async viewasync def view(): ...
Wait on I/Oawait coro()
Overlap many waitsawait asyncio.gather(...)
Async HTTPhttpx.AsyncClient()
Serve under ASGIWsgiToAsgi(app) + Hypercorn

🎯 Your Turn: Fill in the Blanks

Complete the route below. Replace each ___ so it's an async view that awaits and returns JSON.

# 🎯 YOUR TURN: replace each ___ to finish the async view
import asyncio
from flask import Flask, jsonify

app = Flask(__name__)

@app.route("/ping")
___ def ping():                 # keyword that makes this an async view
    ___ asyncio.sleep(0)        # keyword that waits on the coroutine
    return jsonify(pong=True)

with app.test_client() as c:
    print(c.get("/ping").status_code)   # 200
    print(c.get("/ping").get_json())    # {'pong': True}

Common Errors & Quick Reference

❌ "Install Flask with the 'async' extra"

Async views need asgiref. Run pip install "flask[async]" so Flask can run async def views on an event loop.

❌ The async view is no faster than a sync one

You awaited a blocking call (like requests.get or a sync DB query). Blocking calls freeze the event loop. Use async libraries (httpx.AsyncClient) or run blocking work in a thread executor.

ConceptMeaning
WSGISync interface; one request per worker
ASGIAsync interface; many concurrent connections
I/O-boundWaiting on network/disk — async helps
CPU-boundCrunching numbers — async does not help

🏆 Mini Challenge: A Concurrent Portfolio

Fetch several prices concurrently and combine them in an async view.

# 🏆 Mini Challenge: fan out concurrent fetches and combine them
import asyncio
from flask import Flask, jsonify

app = Flask(__name__)

async def price_of(symbol):
    await asyncio.sleep(0)                 # pretend: await an API
    prices = {"AAPL": 190, "GOOG": 140, "MSFT": 410}
    return symbol, prices[symbol]

@app.route("/portfolio")
async def portfolio():
    pairs = await asyncio.gather(
        price_of("AAPL"), price_of("GOOG"), price_of("MSFT")
    )
    prices = dict(pairs)
    return jsonify(prices=prices, total=sum(prices.values()))

with app.test_client() as c:
    data = c.get("/portfolio").get_json()
    print(data["prices"])   # {'AAPL': 190, 'GOOG': 140, 'MSFT': 410}
    print(data["total"])    # 740

❓ Frequently Asked Questions

Lesson complete — you can overlap slow I/O in a view!

You wrote async def views, overlapped waits with asyncio.gather, awaited an async client, and learned WSGI's one-request limit and the ASGI deployment that unlocks real concurrency.

Practice quiz

How do you define an async view in Flask?

  • with @app.async_route
  • with async def
  • with @asyncio.view
  • with await def

Answer: with async def. An async view is a route function defined with async def.

Since which version has Flask supported async views?

  • Flask 0.12
  • Flask 1.0
  • Flask 2.0+
  • Flask never supports them

Answer: Flask 2.0+. Flask 2.0+ supports async def views, running them on an event loop.

What must you install for async views?

  • flask[async]
  • flask-async-extension
  • asyncio separately
  • uvicorn only

Answer: flask[async]. Install the async extra with pip install 'flask[async]', which adds asgiref.

When does async actually help?

  • CPU-bound number crunching
  • A single quick query
  • I/O-bound work with overlapping waits
  • Rendering templates

Answer: I/O-bound work with overlapping waits. Async shines for I/O-bound work where awaits can overlap.

Which call runs several awaitable I/O operations concurrently?

  • asyncio.gather(...)
  • asyncio.run_all(...)
  • await sequentially
  • thread.join()

Answer: asyncio.gather(...). asyncio.gather runs the awaitables concurrently.

Does an async view make plain WSGI Flask non-blocking across requests?

  • Yes, fully non-blocking
  • No, the worker still handles one request at a time
  • Only with threads
  • Only in debug mode

Answer: No, the worker still handles one request at a time. Under WSGI the worker still serves one request at a time; awaits overlap only within a request.

What is ASGI?

  • A database driver
  • A template engine
  • A caching backend
  • The async successor to WSGI

Answer: The async successor to WSGI. ASGI is the asynchronous successor to WSGI.

How do you wrap a Flask app to serve it under ASGI?

  • WsgiToAsgi(app) from asgiref
  • Flask(async=True)
  • app.to_asgi()
  • asyncio.wrap(app)

Answer: WsgiToAsgi(app) from asgiref. asgiref's WsgiToAsgi(app) wraps the WSGI app for an ASGI server.

Which client works correctly with await in an async view?

  • requests.get
  • urllib.request
  • httpx.AsyncClient
  • a synchronous DB query

Answer: httpx.AsyncClient. await works with async-aware libraries like httpx.AsyncClient.

What happens if you await a blocking call like requests.get in an async view?

  • It becomes async automatically
  • It blocks the event loop
  • Flask raises an error
  • It runs in a separate process

Answer: It blocks the event loop. A blocking call still blocks the event loop even inside an async view.

Continue this course

Frequently asked questions

What is an async view in Flask?

An async view is a route function defined with async def instead of def. Inside it you can use await to call asynchronous code. Flask 2.0+ supports them: it runs each async view on an event loop for you. You need the async extra installed (pip install 'flask[async]'), which adds the asgiref dependency.

When does async actually help?

Async shines for I/O-bound work where a request spends time waiting — calling several external APIs, querying multiple services, or other awaitable I/O. With await and asyncio.gather you can run those waits concurrently instead of one after another. For CPU-bound work or a single quick query, async adds no benefit.

Does an async view make Flask non-blocking?

Not by itself. Standard Flask runs under WSGI, which handles one request per worker. An async view runs on a temporary event loop inside that worker, so awaits within a single request can overlap, but the worker still handles one request at a time. To serve many concurrent connections you run under an ASGI server.

What is ASGI and how is it different from WSGI?

WSGI is the synchronous Python web-server interface Flask traditionally uses. ASGI is its asynchronous successor, designed for async frameworks and long-lived connections like WebSockets. To get true concurrency from async views you wrap your Flask app with asgiref's WsgiToAsgi and serve it with an ASGI server such as Hypercorn or Uvicorn.

Can I use await with database or requests libraries?

Only with async-aware libraries. await works with httpx.AsyncClient, async database drivers, and asyncio APIs. A blocking call like requests.get or a synchronous DB query does not become async just because it's in an async view — it will still block the event loop. Use the async variants, or run blocking calls in a thread executor.