Async Python: Why Your Programs Are Slow and How to Fix Them
A practical Python async/await tutorial: understand why synchronous code is slow for I/O tasks and how asyncio, async def, and await make programs dramatically faster.
Get more content like this on Telegram!
Daily AI tips, notes & resources — free
Async Python: Why Your Programs Are Slow and How to Fix Them
I had a script that needed to check the status of 200 URLs and report which ones were down. Synchronous version: 4 minutes. Async version: 8 seconds.
That's not a cherry-picked example. That's typical for I/O-bound work.
This tutorial explains why synchronous Python is slow for tasks like this, what async/await actually means, and how to write async code that dramatically speeds up I/O-bound programs.
Why Your Synchronous Code Is Slow
Consider fetching 5 URLs synchronously:
import requests
import time
urls = [
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
]
start = time.time()
responses = []
for url in urls:
response = requests.get(url)
responses.append(response.status_code)
print(f"Took {time.time() - start:.1f}s")
# Output: Took 5.2s
Each request takes ~1 second. With 5 requests: ~5 seconds. With 200 requests: ~200 seconds.
Why? During each request, Python is just waiting — waiting for the server to respond. Your CPU is idle. Your program is blocked.
This is I/O-bound work: the bottleneck is waiting for external data, not computation.
The Async Solution
import asyncio
import aiohttp
import time
async def fetch(session, url):
async with session.get(url) as response:
return response.status
async def fetch_all(urls):
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
return await asyncio.gather(*tasks)
urls = ["https://httpbin.org/delay/1"] * 5
start = time.time()
results = asyncio.run(fetch_all(urls))
print(f"Took {time.time() - start:.1f}s")
# Output: Took 1.1s
Same 5 requests. All running concurrently. Finished in ~1 second instead of ~5.
pip install aiohttp
Understanding async/await
What async def Does
async def my_coroutine():
return "hello"
Adding async to a function makes it a coroutine. Calling it doesn't execute it immediately — it returns a coroutine object. You have to await it or run it with asyncio.run().
# This does NOT run the function:
result = my_coroutine() # returns a coroutine object
# This DOES run it:
result = await my_coroutine() # only valid inside another async function
# OR
result = asyncio.run(my_coroutine()) # valid at top level
What await Does
await tells Python: "Start this operation, and while waiting for it to complete, go do something else."
async def fetch_data():
print("Starting request...")
await asyncio.sleep(1) # Simulate waiting for network
print("Request done!")
return "data"
When Python hits await asyncio.sleep(1), instead of blocking for 1 second, the event loop can run other coroutines.
The Event Loop
The event loop is the engine that runs async code. It manages a queue of coroutines and schedules them:
async def main():
# Run two coroutines concurrently
result1, result2 = await asyncio.gather(
fetch_data(),
fetch_data(),
)
asyncio.run(main()) # Creates an event loop and runs main()
asyncio.gather() — The Key to Concurrency
asyncio.gather() runs multiple coroutines concurrently and waits for all of them:
import asyncio
async def task(name, duration):
print(f"{name}: starting (will take {duration}s)")
await asyncio.sleep(duration)
print(f"{name}: done")
return f"{name} result"
async def main():
start = time.time()
# Run 3 tasks concurrently
results = await asyncio.gather(
task("Task A", 2),
task("Task B", 1),
task("Task C", 3),
)
print(f"All done in {time.time() - start:.1f}s")
print(results)
asyncio.run(main())
# Output:
# Task A: starting (will take 2s)
# Task B: starting (will take 1s)
# Task C: starting (will take 3s)
# Task B: done
# Task A: done
# Task C: done
# All done in 3.0s ← Not 6s! They ran concurrently.
Total time = longest task (3s), not sum of all tasks (6s).
Real-World Example: Fetch Multiple APIs Concurrently
import asyncio
import httpx
import json
async def fetch_user(client: httpx.AsyncClient, user_id: int) -> dict:
response = await client.get(f"https://jsonplaceholder.typicode.com/users/{user_id}")
return response.json()
async def fetch_all_users(user_ids: list[int]) -> list[dict]:
async with httpx.AsyncClient() as client:
tasks = [fetch_user(client, uid) for uid in user_ids]
users = await asyncio.gather(*tasks)
return users
async def main():
user_ids = list(range(1, 11)) # Users 1-10
start = time.time()
users = await fetch_all_users(user_ids)
elapsed = time.time() - start
print(f"Fetched {len(users)} users in {elapsed:.2f}s")
for user in users:
print(f" {user['name']}: {user['email']}")
asyncio.run(main())
pip install httpx
Async Database Queries
import asyncio
import aiosqlite
async def get_users():
async with aiosqlite.connect("app.db") as db:
async with db.execute("SELECT id, name FROM users") as cursor:
return await cursor.fetchall()
async def main():
users = await get_users()
for user in users:
print(user)
asyncio.run(main())
pip install aiosqlite
Async with FastAPI
FastAPI is built on asyncio. Using async def in route handlers lets FastAPI handle many requests concurrently on a single thread:
from fastapi import FastAPI
import httpx
app = FastAPI()
@app.get("/weather/{city}")
async def get_weather(city: str):
async with httpx.AsyncClient() as client:
response = await client.get(f"https://wttr.in/{city}?format=j1")
return response.json()
If your FastAPI route makes database queries or external API calls, use async versions of those libraries. For more on FastAPI, see our FastAPI REST API tutorial.
When NOT to Use Async
CPU-bound tasks: If your code does heavy computation (number crunching, image processing, training ML models), async won't help — it only helps with I/O waits. Use multiprocessing instead.
# Don't async-ify CPU work:
import multiprocessing
def process_chunk(data):
return sum(x ** 2 for x in data) # CPU-bound
with multiprocessing.Pool() as pool:
results = pool.map(process_chunk, data_chunks)
Simple scripts: A one-time script that makes 3 API calls doesn't need async. The overhead of learning and implementing it isn't worth it for trivial concurrency.
Rules of thumb:
- < 10 concurrent I/O operations: synchronous is fine
- 10–1,000 concurrent I/O: async is valuable
- CPU-heavy tasks: multiprocessing
Common Async Mistakes
Mistake 1: Using sync libraries in async code
# WRONG — blocks the event loop
async def bad():
response = requests.get(url) # This blocks everything!
# CORRECT
async def good():
async with httpx.AsyncClient() as client:
response = await client.get(url)
Mistake 2: Forgetting await
async def fetch():
return await some_async_call() # ✓
async def broken():
return some_async_call() # Returns coroutine object, not result
Mistake 3: Running event loops inside event loops
# If you're already inside an async context, don't call asyncio.run()
# Use await instead
Frequently Asked Questions
When should I use async Python?
I/O-bound tasks: HTTP calls, database queries, file reads. Not for CPU-heavy computation.
Async vs threads?
Both handle I/O concurrency. Async is single-threaded and explicit; easier to reason about. Threads are preemptive with OS scheduling. Async is preferred in modern Python.
What is asyncio?
Python's built-in async library. Provides the event loop, gather(), sleep(), and async primitives.
How do I make async HTTP requests?
Use aiohttp or httpx (both have async clients). Don't use requests (synchronous only).
Final Thoughts
Async Python is one of those tools that changes how you think about programs. Once you understand that await means "pause here and let other work happen," the whole model clicks into place.
The practical rule: reach for async when you're making many I/O calls. A FastAPI server handling 1,000 requests, a scraper fetching 500 pages, a data pipeline calling 200 external APIs — these are the right use cases.
For the Python libraries that work best with async patterns, see our best Python libraries guide which covers httpx, aiohttp, and the async ecosystem. And for automation scripts that benefit from concurrent I/O, our Python automation scripts guide shows patterns you can make faster with async.
Frequently Asked Questions
AiTechWorlds Team
✓ Verified WriterThe AiTechWorlds team is passionate about AI, technology, and education. We create high-quality, research-backed content to help you learn, grow, and succeed in the modern digital world.
Related Articles
The Python Libraries Every Developer Must Know in 2025
The essential Python libraries for 2025: from requests and pandas to FastAPI and LangChain — what each does, when to use it, and how to get started quickly.
Django vs Flask in 2025: Which Framework Should You Learn?
An honest Django vs Flask comparison for 2025 — which Python framework to learn first, when each excels, and why FastAPI has changed the equation.
FastAPI Tutorial: Building Your First REST API in 30 Minutes
A hands-on FastAPI tutorial for beginners: build a fully functional REST API in 30 minutes with CRUD endpoints, request validation, and automatic docs.
Jupyter Notebook Guide: The Data Scientist's Favorite Tool
A complete Jupyter Notebook guide for 2025: installation, essential shortcuts, best practices, and how data scientists use Jupyter for exploration, analysis, and sharing.