Day 3:Asynchronous Python (`asyncio`), The Event Loop, `gather()` & Rate-Limiting Math
Battle-tested daily lesson note • 100% runnable code, interview Q&A, test suite & interactive quiz
Week 2 — Day 3: Asynchronous Python (asyncio), The Event Loop, gather() & Rate-Limiting Math
Mentor's Note (10+ YOE Staff AI Engineer POV): Why is
asynciorequired knowledge for every single AI Engineering job description? Because LLM API calls, Vector Database searches, and MCP Tool executions are 99% I/O-bound (Waiting on the Network)!Look at the math: Suppose your evaluation suite (Month 2 Capstone) or batch RAG ingestion (Month 3 Capstone) needs to run 50 LLM queries, and each LLM call takes of network wait time:
- Synchronous (
forloop withrequests): During 99.9% of those 75 seconds, your 12-core CPU is sitting completely idle waiting for HTTP packets!- Asynchronous (
asyncio.gatherwith Concurrency Limit viaasyncio.Semaphore(10)):
Part 1: CPU-Bound vs I/O-Bound & Python's GIL (Global Interpreter Lock)
| Workload Type | What the Computer is Doing | Example in AI Engineering | Right Concurrency Tool |
|---|---|---|---|
| I/O-Bound (Input/Output) | Waiting for an external network response or disk read/write (0% CPU usage while waiting). | Calling OpenAI / Gemini API, querying Qdrant / PostgreSQL, fetching web pages. | asyncio (Single-threaded Event Loop) — Can juggle 10,000+ concurrent network waits on 1 thread! |
| CPU-Bound | Crunching heavy math continuously on the CPU cores (100% CPU usage). | Tokenizing 5 GB of text locally, training a decision tree, parsing massive PDFs. | multiprocessing (ProcessPoolExecutor) — Spawns separate OS processes to bypass the GIL across multiple CPU cores. |
[!IMPORTANT] What is the GIL (Global Interpreter Lock)? In standard CPython, a mutex lock called the GIL allows only one thread to execute Python bytecode at a time inside a single OS process. That is why multi-threading (
threading) does not speed up pure-Python CPU math—butasynciodoesn't care about the GIL because while waiting for OpenAI/Gemini over the network, the OS socket handles the wait and the Event Loop switches to start the next request immediately!
Part 2: How the asyncio Event Loop Works Visually
SYNCHRONOUS (Blocking):
Task 1: [Send Req 1] ──────(Wait 1.5s doing nothing)──────> [Recv 1]
Task 2: [Send Req 2] ──────(Wait 1.5s)──────> [Recv 2]
Total Time = 3.0s
ASYNCHRONOUS (Non-Blocking Event Loop on 1 Single Thread!):
Task 1: [Send Req 1] ──(yields control at `await`!)──────────────> [Recv 1]
Task 2: [Send Req 2] ──(yields control at `await`!)───> [Recv 2]
Total Time = 1.5s!
The 4 Core asyncio Primitives
async def my_func():Declares a Coroutine Function. Callingmy_func()does NOT run it yet—it returns a Coroutine Object (just likeyieldreturns a Generator!).await coro: Pauses the current coroutine untilcorocompletes, yielding control back to the Event Loop so other tasks can run in the meantime!asyncio.gather(*tasks, return_exceptions=True): Schedules multiple coroutines concurrently on the Event Loop and waits for all of them to finish, returning their results in the exact original order!asyncio.Semaphore(max_concurrent): Acts as a bouncer at a club door! If you launch 1,000 LLM calls at the exact same millisecond without a Semaphore, OpenAI/Gemini will immediately block you withHTTP 429 Too Many Requests (Rate Limit Exceeded). Wrapping calls inasync with semaphore:guarantees at mostCrequests are in flight simultaneously!
[!CAUTION] The #1
asyncioDeadly Sin: Callingtime.sleep()orrequests.get()insideasync def! If you writetime.sleep(2)(instead ofawait asyncio.sleep(2)) orrequests.get()(instead ofawait httpx_client.get()) inside anasync deffunction, you freeze the entire Event Loop thread! All 1,000 concurrent users on your FastAPI server will freeze for 2 seconds becausetime.sleep()never saysawaitto yield control back to the Event Loop!
Part 3: Hands-On Lab — Concurrent Rate-Limited LLM Batch Engine (async_batch_engine.py)
Check out src/async_batch_engine.py (in Tab 2: Codes — click Run in Browser to execute) and run the live sequential vs concurrent benchmark and unit tests:
python week02_advanced_python_pydantic_asyncio_tests/day03_asyncio_concurrency_event_loop/src/async_batch_engine.py
python -m unittest discover -s week02_advanced_python_pydantic_asyncio_tests/day03_asyncio_concurrency_event_loop/tests -v