šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

Python asyncio Fundamentals

The event loop model behind asyncio — cooperative multitasking that handles thousands of concurrent I/O operations on a single thread, and why it scales where threading doesn't.

⚔ Total XP: 0|šŸ’» python XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

System Hub

Core logic.

Quick Quiz //

Why does asyncio.gather(fetch("A", 2), fetch("B", 1), fetch("C", 1.5)) take about 2 seconds total, not 4.5?


šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

asyncio is Python's answer to high-concurrency I/O-bound workloads — web servers handling thousands of simultaneous connections, clients making hundreds of concurrent API calls. This lesson builds the mental model of the event loop and cooperative multitasking from the ground up, before the next lesson covers async/await syntax itself.

1Cooperative Multitasking: One Thread, Voluntary Yielding

asyncio achieves concurrency without multiple threads or processes by using cooperative multitasking: a single thread runs an event loop, which keeps track of every currently-running coroutine (an async def function) and switches between them — but only at points where a coroutine explicitly says 'I'm waiting on something, you can run someone else now.' That explicit yield point is the await keyword; await asyncio.sleep(delay) doesn't just pause the current coroutine, it hands control back to the event loop, which immediately looks for other ready work (like the other fetch() calls) to run in the meantime.

This is fundamentally different from threading's preemptive model, where the operating system can interrupt a thread at essentially any point to let another thread run, whether that thread wants to yield or not. asyncio's cooperative model has no such safety net — a coroutine that never awaits anything runs to completion without ever giving the event loop a chance to switch to other work, which is exactly what bad_task() demonstrates: its 50-million-iteration loop has no await inside it, so it monopolizes the single thread for its entire duration, freezing every other task, including ones that would otherwise be ready to make progress.

This is the single most important mental model shift required to use asyncio correctly: concurrency isn't automatic just because a function is declared async def — a coroutine only actually shares the thread cooperatively at its await points, and a CPU-heavy coroutine with none of those points behaves exactly like a synchronous function that blocks everything else.

āœ•
—
+
import asyncio

async def fetch(name: str, delay: float):
    print(f"{name}: starting")
    await asyncio.sleep(delay)   # yields control back to the event loop
    print(f"{name}: done")

async def main():
    await asyncio.gather(fetch("A", 2), fetch("B", 1), fetch("C", 1.5))

asyncio.run(main())
# All three 'start' immediately; total time is ~2s (the SLOWEST), not 4.5s (the sum)
localhost:3000
Event Loop Scheduling
asyncio.gather(fetch A/B/C)
~2s total — overlapping wait times, single thread

2Why This Scales Further Than Threading for I/O

Each OS thread carries real, fixed overhead — its own stack (typically megabytes, even if mostly unused), and the operating system's own scheduling and context-switching cost, which grows as the number of threads grows. A process handling a few dozen concurrent connections via threading is manageable; a few thousand threads for a few thousand concurrent connections becomes a genuine resource problem, in memory and in scheduling overhead alone.

asyncio's coroutines are dramatically lighter weight than OS threads — they're plain Python objects tracked by the event loop, not OS-level constructs, so a single process can juggle tens of thousands of concurrent coroutines with a fraction of the memory and scheduling cost thousands of real threads would require. This is precisely why high-throughput async web frameworks (FastAPI, for instance) can handle far more simultaneous connections per server than an equivalent threaded design, for workloads that are genuinely I/O-bound.

The trade-off for that scalability is the requirement, introduced in the next scene, that the *entire* call chain from the event loop down needs to be async-aware — a single blocking, synchronous call buried inside otherwise-async code freezes the whole event loop for its duration, a failure mode that simply doesn't exist for threading, where one slow synchronous call only blocks its own thread, not every other thread's work.

āœ•
—
+
async def bad_task():
    total = sum(i * i for i in range(50_000_000))  # NO await here
    return total

# While bad_task() runs, EVERY other async task is frozen -- 
# there's no preemption to interrupt it, unlike OS-scheduled threads
localhost:3000
Scalability Comparison
Thousands of coroutines
vs. thousands of OS threads — dramatically less memory/scheduling overhead

3The All-or-Nothing Requirement: Async-Aware Libraries Throughout

requests.get(url) is a synchronous, blocking call — internally, it doesn't await anything, because requests was written before asyncio existed and has no concept of yielding control to an event loop. Calling it from inside an async def function doesn't make it non-blocking; it simply blocks the entire single thread the event loop is running on for however long the request takes, exactly like bad_task()'s CPU-heavy loop — from the event loop's perspective, a blocking network call and a blocking computation are indistinguishable; both monopolize the thread with no yield point.

httpx.AsyncClient (or aiohttp), by contrast, is built specifically to integrate with the event loop: await client.get(url) performs the network operation using non-blocking I/O primitives under the hood, and yields control back to the event loop for the duration of the wait, exactly like asyncio.sleep() does — letting other coroutines make progress during that time.

This is the practical reason asyncio adoption is often 'all or nothing' within a given call chain: mixing a single blocking library call into an otherwise fully-async codebase silently reintroduces the exact blocking behavior asyncio exists to avoid, without raising any error — the code runs, but the concurrency benefit for that code path quietly disappears. Choosing asyncio for a project means committing to async-aware libraries (httpx over requests, asyncpg or an async ORM over a synchronous database driver) throughout the I/O-bound parts of the call chain.

āœ•
—
+
import asyncio, requests  # requests is SYNCHRONOUS, not async-aware

async def fetch_blocking(url):
    return requests.get(url)  # blocks the ENTIRE event loop while waiting

import httpx  # httpx has a proper async client

async def fetch_async(url):
    async with httpx.AsyncClient() as client:
        return await client.get(url)  # yields control while waiting
localhost:3000
Blocking vs Non-Blocking
requests.get() inside async def
Blocks the whole event loop — no yield point exists

4Step-by-Step Breakdown

A single asyncio event loop can juggle thousands of concurrent network connections on one thread. Understanding HOW it does that is the key to writing async code that actually works.

asyncio runs everything on ONE thread, using an event loop that switches between tasks whenever one is waiting on I/O — cooperative, not preemptive.

Checkpoint: Why does asyncio.gather(fetch("A", 2), fetch("B", 1), fetch("C", 1.5)) take about 2 seconds total, not 4.5?

  • →All three tasks run concurrently on the event loop, so total time is the slowest one, not the sum
  • →Each task runs on a genuinely separate CPU core in parallel

'Cooperative' means a task must explicitly yield control (via await) — a task that never awaits anything blocks the ENTIRE event loop, including every other task.

Checkpoint: What happens to every other async task while bad_task() runs its 50-million-iteration sum with no await inside it?

  • →They are all frozen — cooperative multitasking means nothing else runs until bad_task() yields control or finishes
  • →They continue running normally in the background

This is why asyncio needs async-AWARE libraries throughout — a blocking call like requests.get() (not httpx's async client) still freezes the whole loop.

With the event loop model in place, async/await is the syntax that lets you write cooperative, non-blocking code that reads almost like ordinary synchronous Python.

Prove Real Concurrent Scheduling. Finish main(): gather() schedules every coroutine up front, before awaiting any of them.

Level Up šŸš€

Advanced cheat sheets, SEO tricks, and interview prep for this topic.

Browser Support

ChromeSupported

Fully supported (via server-side Python execution).

FirefoxSupported

Fully supported (via server-side Python execution).

SafariSupported

Fully supported (via server-side Python execution).

EdgeSupported

Fully supported (via server-side Python execution).

Best Practices

Use only async-aware libraries for I/O inside async code — never a blocking synchronous call

A single blocking call inside an async function freezes the entire event loop for its duration, silently defeating the concurrency asyncio is meant to provide, with no error raised to warn you.

Remember that CPU-heavy work inside a coroutine still blocks every other task

async def alone provides no CPU parallelism or preemption — genuinely CPU-bound work inside async code should be offloaded to a thread/process pool (via loop.run_in_executor) rather than run inline.

Frequent Bugs

THE BUG

Calling a synchronous, blocking library function (like requests.get or a synchronous DB driver) from inside an async def function, silently freezing the event loop and eliminating concurrency for that code path with no error raised.

THE FIX

Replace blocking calls with their async-aware equivalents (httpx.AsyncClient instead of requests, an async database driver) so the call actually yields control at its await point instead of blocking the thread.

Real-World Examples

Fetching Data From Multiple Microservices Concurrently

An API gateway needs to call three internal microservices to assemble a single response, and doing so sequentially adds each service's latency together instead of overlapping them.

import asyncio
import httpx

async def fetch_json(client: httpx.AsyncClient, url: str) -> dict:
    response = await client.get(url)
    return response.json()

async def get_dashboard_data():
    async with httpx.AsyncClient() as client:
        user, orders, inventory = await asyncio.gather(
            fetch_json(client, USER_SERVICE_URL),
            fetch_json(client, ORDERS_SERVICE_URL),
            fetch_json(client, INVENTORY_SERVICE_URL),
        )
    return {"user": user, "orders": orders, "inventory": inventory}

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Calling a blocking, synchronous library function directly inside an async def function, silently freezing the entire event loop for the duration of that call with no error or warning.

# Wrong: blocks the entire event loop async def fetch(url): return requests.get(url) # synchronous, no await point # Correct: yields control while waiting async def fetch(client, url): return await client.get(url) # httpx.AsyncClient

The Solution //

Use the async-aware equivalent library (httpx instead of requests, an async DB driver) so the call actually has an await point that yields control back to the event loop.

Lesson Glossary

[01]Event loop

asyncio's central scheduler, running on a single thread, that manages and switches between coroutines at their await points.

Code Preview
// Event loop context

[02]Cooperative multitasking

A concurrency model where tasks voluntarily yield control (via await) rather than being preemptively interrupted by the scheduler.

Code Preview
// Cooperative multitasking context

[03]Coroutine

An async def function; calling it returns a coroutine object that must be awaited or scheduled to actually run.

Code Preview
// Coroutine context

[04]Blocking call

A synchronous operation with no yield point, which monopolizes the event loop's single thread for its full duration if called from async code.

Code Preview
// Blocking call context

Continue Learning