Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →

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.

A
AiTechWorlds Team
May 27, 2026 7 min read
📱

Get more content like this on Telegram!

Daily AI tips, notes & resources — free

Join 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.

Share this article:

Frequently Asked Questions

Use async Python when your program spends significant time waiting on I/O operations: HTTP requests, database queries, file reads, or network communication. If you're making 100 API calls sequentially, async lets you make them concurrently — 100x faster in theory, typically 10–50x in practice. Don't use async for CPU-bound work (calculations, data processing) — for that, use multiprocessing instead. The rule: async for I/O bound tasks, multiprocessing for CPU-bound tasks.
A

AiTechWorlds Team

✓ Verified Writer

The 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

10K+ Members Growing Daily

Get Free AI Notes Daily

Join AiTechWorlds on Telegram and get daily AI tips, prompt engineering templates, coding resources, and exclusive content — 100% free!

📚 Free Study Notes🤖 AI Tips Daily⚡ Prompt Templates💻 Coding Resources
Join Free Channel

No spam. Leave anytime.

!