Flask
async requests
concurrency
Python web development
server-side programming

How to SEND multiple requests ASYNC from Flask server?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

If one Flask endpoint needs data from several upstream services, sending those outbound calls concurrently can reduce the total response time. The important qualification is that Flask has historically been a WSGI framework, so “async from Flask” can mean two different designs: a modern async view with an async HTTP client, or a conventional synchronous view that uses threads to fan out blocking requests.

Async View with httpx

Recent Flask versions allow async route handlers. When the route itself is async def, an async client such as httpx.AsyncClient gives the cleanest implementation.

python
1from flask import Flask, jsonify
2import asyncio
3import httpx
4
5app = Flask(__name__)
6
7async def fetch_one(client, url):
8    response = await client.get(url, timeout=5.0)
9    return {"url": url, "status": response.status_code}
10
11@app.get("/aggregate")
12async def aggregate():
13    urls = [
14        "https://example.com",
15        "https://example.org",
16    ]
17
18    async with httpx.AsyncClient() as client:
19        results = await asyncio.gather(
20            *(fetch_one(client, url) for url in urls)
21        )
22
23    return jsonify(results)

That structure lets all outbound requests progress concurrently instead of one after another.

Why Async Syntax Alone Is Not Enough

Writing async def does not automatically mean the whole stack benefits from asynchronous execution. The HTTP client must also be non-blocking, and the deployment model has to support the concurrency you expect.

The practical decision is usually:

  • async view plus async client for modern async-capable setups
  • sync view plus thread pool for classic Flask WSGI deployments

If the whole application is deeply async and I O bound, an ASGI-native framework may fit better than stretching Flask to handle everything.

Handle Failure Behavior Explicitly

When several upstream calls run together, you need a policy for partial failure. By default, asyncio.gather raises if one task fails.

python
1results = await asyncio.gather(
2    *(fetch_one(client, url) for url in urls),
3    return_exceptions=True,
4)

With return_exceptions=True, the endpoint can inspect each result and decide whether to return partial data, a degraded response, or a full error. That decision belongs to the application, not to the concurrency primitive.

Synchronous Flask Can Still Fan Out Concurrently

If the app remains synchronous, you can still overlap outbound requests with a thread pool and a blocking client such as requests.

python
1from concurrent.futures import ThreadPoolExecutor
2from flask import Flask, jsonify
3import requests
4
5app = Flask(__name__)
6pool = ThreadPoolExecutor(max_workers=5)
7
8def fetch_one(url):
9    response = requests.get(url, timeout=5.0)
10    return {"url": url, "status": response.status_code}
11
12@app.get("/aggregate-sync")
13def aggregate_sync():
14    urls = ["https://example.com", "https://example.org"]
15    futures = [pool.submit(fetch_one, url) for url in urls]
16    return jsonify([f.result() for f in futures])

This is not async and await internally, but it still solves the latency problem in many ordinary Flask deployments.

Timeouts and Concurrency Limits Matter

Concurrent fan-out is useful, but it also increases pressure on your app and on the upstream systems. Always define:

  • request timeouts
  • a cap on concurrent outbound calls
  • logging or metrics for slow upstream dependencies

Without those controls, one incoming request can trigger too many outgoing requests and create a cascading latency problem.

Reuse Clients When Appropriate

If every endpoint invocation creates a new client or a new thread pool unnecessarily, concurrency gains can be offset by overhead. For many apps, it is better to keep a reusable client or bounded pool at the application level and use it consistently.

That keeps connection management and concurrency policy in one place instead of scattering it across handlers.

Testing the Real Behavior

The correct test is not “does the code run.” The correct test is “does the endpoint complete faster than sequential fan-out under realistic latency.” Use slow upstream stubs or test servers and compare the measured response time.

That gives you evidence that the concurrency model is actually helping instead of just making the code look more modern.

Common Pitfalls

  • Writing an async Flask route but using a blocking HTTP client inside it.
  • Expecting async syntax alone to improve performance without matching deployment support.
  • Ignoring timeouts and concurrency limits on outbound calls.
  • Failing to decide what the endpoint should return when one upstream request fails.
  • Using Flask for deeply async orchestration when an ASGI-native framework may be a better fit.

Summary

  • Flask can fan out multiple outbound requests concurrently, but the pattern depends on the deployment model.
  • For async routes, pair async def with an async HTTP client such as httpx.AsyncClient.
  • For classic synchronous Flask, a thread pool can still provide useful concurrency.
  • Add timeouts, limits, and failure handling from the start.
  • Measure the actual latency improvement instead of assuming async syntax is enough.

Course illustration
Course illustration

All Rights Reserved.