Async Await
Asynchronous programming is at the heart of modern Python applications. Whether you're building high-performance web APIs, data pipelines, websocket apps, scrapers, or microservices — mastering advanced async/await patterns gives you the ability to build systems that scale effortlessly.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn
This lesson dives beyond the basics. You'll learn event loop mechanics, tasks, concurrency patterns, async iterators, async generators, synchronization primitives, and real-world architectures used in production environments.
🔥 1. Understanding the Event Loop Deeply
The Python async system is powered by the event loop , a scheduler that:
What It Does
Why It Matters
Executes async tasks
Runs your async def functions
Handles IO events
Network, file, database operations
Manages task switching
Pauses waiting tasks, runs ready ones
Runs callbacks & timers
Scheduled operations and delays
- Creates an event loop
- Starts running the coroutine
- Closes the loop after completion
⚡ 2. Coroutine Chaining & Composition
Coroutines can call other coroutines using await :
You can chain dozens of async functions without blocking the thread.
⚙️ 3. Running Tasks Concurrently With asyncio.gather
This is how you run coroutines in parallel (non-blocking):
- Web scraping
- Parallel API calls
- Batch data processing
🧨 4. Creating Background Tasks With asyncio.create_task
Tasks let coroutines run independently in the background:
- Polling loops
- Websocket heartbeats
- Scheduled jobs
🧵 5. Using asyncio.wait() for Advanced Control
Different from gather() , wait() lets you specify:
Option
When to Use
FIRST_COMPLETED
React as soon as ANY task finishes
ALL_COMPLETED
Wait for ALL tasks (like gather)
FIRST_EXCEPTION
Stop immediately if any task fails
- Racing multiple sources
- Timeout systems
- Picking fastest available API
🌀 6. Timeouts With asyncio.wait_for
Critical for resilient systems that depend on external APIs.
🧱 7. Async Context Managers (async with)
Used for resources that need async setup AND cleanup :
Use Case
Why Async?
Database connections
Connect/disconnect takes network time
HTTP clients
Opening/closing sessions is IO
WebSockets
Handshake/teardown is async
🌀 8. Async Iterators (async for)
- File IO wrappers
- Real-time data feeds
🌊 9. Async Generators (async def … yield)
These are perfect for streaming data where each item needs async fetching:
- Websocket data
- Real-time dashboards
- Server push notifications
🔒 10. Async Locks, Semaphores & Synchronization
Prevents race conditions when multiple tasks access shared data:
⚡ 11. Producer–Consumer Pattern (Async Pipeline)
- AI data pipelines: Load data → Process → Save
- Web crawlers: Discover URLs → Fetch pages → Extract data
- Background jobs: Receive requests → Queue → Process
🧠 12. Parallelism vs Concurrency
Concept
What It Means
Real-World Example
Concurrency
Handling multiple tasks by switching between them
One chef cooking 3 dishes, switching between them
Parallelism
Actually doing multiple things at the same time
Three chefs each cooking one dish simultaneously
- asyncio → IO-bound (network, files, database)
- multiprocessing → CPU-bound (math, ML, image processing)
- concurrent.futures → Mixed workloads
A professional engineer must know when to choose which.
🌐 13. Combining Async With APIs (Practical)
This lets you send 1000 requests concurrently without blocking.
🔍 14. Async Patterns Used in Real Web Frameworks
FastAPI, Starlette, aiohttp, Quart — all rely on:
- async routers
- async database sessions
- async locks for shared state
- async iterators for streaming responses
🧩 15. Full Architecture Demo (Advanced)
- async file readers
- async DB connections
- async external API fetchers
- queues (producer/consumer)
- batch processors
- background tasks
- cancellation handlers
Async lets each component run without blocking any other.
🔥 16. Understanding Task Cancellation in Depth
Tasks can be cancelled, but you must handle the cancellation gracefully:
- ✔ graceful shutdowns
- ✔ web servers closing requests
- ✔ stopping background loops
- ✔ cleaning up resources
If you don't handle CancelledError, tasks become "dangling zombies" and corrupt state.
🧠 17. Task Groups (Python 3.11+) — Structured Concurrency
TaskGroups improve error handling and cancellation. If one task inside a group fails, the entire group is safely cancelled.
- ✔ predictable cancellation
- ✔ easier debugging
- ✔ no silent orphan tasks
- ✔ safer microservices
- ✔ recommended by Python core devs
TaskGroups will replace asyncio.gather() in most future architectures.
⚙️ 18. Async Error Handling (Fail-Fast, Fail-Safe, Recovering)
Case 2 — fail safe (continue, collect errors):
Used in microservices to avoid hammering broken APIs.
🧵 19. Avoiding Deadlocks in Async Code
Deadlocks occur when tasks wait on each other incorrectly.
- ✔ forgetting await
- ✔ acquiring multiple locks
- ✔ circular waiting
- ✔ blocking calls inside async code
🔒 20. Using Queues for Safe Concurrency
Async queues give safe producer/consumer flow.
- ✔ background job systems
- ✔ AI pipeline batching
- ✔ distributed crawlers
- ✔ video/audio frame processing
- ✔ websocket message routing
Queues stop you from overwhelming your system.
📡 21. Building Real-Time Streams With Async Generators
Example — streaming Bitcoin prices, logs, or live chat updates:
- ✔ real-time dashboards
- ✔ monitoring systems
- ✔ live analytics
- ✔ trading bots
🧩 22. Understanding Cooperative Multitasking
Your code must yield control to let other tasks run:
⚗️ 23. Mixing AsyncIO With Sync Code (The Right Way)
Sometimes you must call blocking code inside async systems:
- ✔ image processing
- ✔ file compression
- ✔ pandas computations
- ✔ machine learning prediction (non-async)
to_thread() prevents freezing the event loop.
📦 24. Handling Bounded Parallelism (Prevent Overload)
Running thousands of tasks at once can overload:
🧬 25. Combining AsyncIO With Multiprocessing
For CPU-heavy workloads (AI / ML / video processing), async alone is not enough.
ProcessPoolExecutor → handles CPU-heavy tasks
- ✔ video rendering
- ✔ neural network inference
- ✔ compression
- ✔ scientific computing
🌍 26. Async File IO (aiofiles)
- ✔ large dataset preprocessing
- ✔ AI training input pipelines
🔁 27. Restartable Background Loops
This is how websocket heartbeats, game loops, monitoring tasks work.
- ✔ Discord bots
- ✔ IoT sensors
- ✔ distributed systems
🎛 28. Backpressure Techniques (Critical for Stability)
Backpressure prevents fast producers from exploding memory.
- ✔ Kafka consumers
- ✔ Redis Streams workers
- ✔ RabbitMQ consumers
- ✔ FastAPI streaming endpoints
🧨 29. Async Retry Strategies With Exponential Backoff
- ✔ ML data ingestion jobs
- ✔ microservice calls
- ✔ database queries
- ✔ message queues
🌐 30. Async Bulk Execution Pattern (High Throughput)
Processing thousands of tasks at controlled speed:
- ✔ bulk API ingestion
- ✔ scraping 10,000 pages
- ✔ ML dataset downloading
- ✔ parallel text generation
🔥 31. Async Caching (In-Memory, Time-Based, and Function-Scoped)
Caching async functions requires async-safe patterns — you cannot use normal functools.lru_cache on coroutines.
- ✔ ML inference caching
- ✔ API rate-limit protection
- ✔ expensive SQL queries
- ✔ metadata caching
- ✔ reusable results in pipelines
⚡ 32. Async LRU Cache (Custom Implementation)
Async LRU caching requires manual implementation:
- ✔ ML preprocessing
- ✔ NLP tokenization
- ✔ thumbnail generation
- ✔ API microservices
- ✔ complex dependency graphs
🌊 33. Async Stream Pipelines (End-to-End Processing)
A professional async pipeline processes streaming data in stages.
- ✔ ETL pipelines
- ✔ log processors
- ✔ real-time analytics
- ✔ AI data loaders
- ✔ chat message routers
Each stage is async → memory safe → fully streaming.
🧩 34. Async + CPU Hybrid Pipelines
- ✔ AI preprocessing
- ✔ video/audio decoding
- ✔ encryption
- ✔ hashing pipelines
- ✔ huge text transformations
📡 35. Async WebSockets — Real-Time Bi-Directional Streams
- ✔ live dashboards
- ✔ multiplayer games
- ✔ IoT communications
WebSockets are the backbone of real-time async systems.
🧵 36. Async Task Supervision (Supervisor Pattern)
Many real systems run supervised workers that restart when they fail.
- ✔ monitoring daemons
- ✔ heartbeat services
- ✔ queue workers
- ✔ real-time scrapers
🔄 37. Async Retry Queues (Automatic Failure Recovery)
- ✔ jobs NEVER disappear
- ✔ failed jobs are retried
- ✔ system never overloads
- ✔ stable long-running services
🧠 38. Backoff, Jitter & Fail-Fast Patterns (Industry Standard)
- ✔ Google Cloud libraries
- ✔ Stripe API clients
- ✔ Redis cluster drivers
🧬 39. Async Cancellation Shields (protect tasks)
Sometimes you want a task to finish even if outer tasks are cancelled.
- ✔ writing logs on shutdown
- ✔ saving ML model state
- ✔ finishing DB transactions
- ✔ flushing message queues
🧪 40. Async Testing Patterns (pytest + asyncio)
🧩 41. Async Resource Pools (DB, HTTP, GPU)
- ✔ database connectors
- ✔ HTTP clients
- ✔ GPU memory managers
- ✔ ML model workers
🛠 42. Combining Async with Redis, Kafka, RabbitMQ
- ✔ event-driven architecture
- ✔ streaming analytics
- ✔ ML log pipelines
🌐 43. Async Microservice Mesh (Framework-Level Example)
- ✔ async router
- ✔ async DB layer
- ✔ async background workers
- ✔ async external API fetchers
- ✔ async timeouts & retries
- ✔ async signals
- ✔ async message queues
This is how modern companies scale to millions of users.
🧠 44. Avoid These async/await Mistakes (Every Beginner Makes Them)
❌ Common Mistake
✅ Correct Fix
time.sleep(1)
await asyncio.sleep(1)
Forgetting await → task never runs
Look for "coroutine was never awaited" warning
Heavy CPU loop inside async
await asyncio.to_thread(func)
Too many parallel tasks at once
Use semaphores for bounded concurrency
🎉 Conclusion
You now understand high-level async engineering concepts:
📋 Quick Reference — Async & Await
Syntax
What it does
async def fn():
Define a coroutine function
await fn()
Pause and wait for a coroutine
asyncio.run(main())
Run the top-level coroutine
asyncio.gather(*coros)
Run coroutines concurrently
asyncio.create_task()
Schedule a coroutine as a Task
You now understand async/await patterns, how to structure concurrent code, and when to use asyncio vs threads.
Up next: AsyncIO Deep Dive — go deeper into the event loop, Tasks, and Futures.
Practice quiz
On how many OS threads does asyncio code run by default?
- One thread per coroutine
- As many threads as CPU cores
- One thread — a single event loop
- It uses processes, not threads
Answer: One thread — a single event loop. AsyncIO is single-threaded concurrency: the event loop switches between tasks on one thread.
When does the event loop switch from one task to another?
- When a task hits an await that yields control
- At random intervals
- Every 10 milliseconds
- Only when a task finishes completely
Answer: When a task hits an await that yields control. Tasks yield control at await points; with no await there is no switch (cooperative multitasking).
What does asyncio.gather(a(), b()) do when a() and b() each await asyncio.sleep(1)?
- Runs them sequentially, taking 2 seconds
- Raises an error
- Runs only the first coroutine
- Runs them concurrently, taking about 1 second
Answer: Runs them concurrently, taking about 1 second. gather runs the coroutines concurrently, so total time is ~1 second, not 2.
What is the correct way to pause for a non-blocking delay inside async code?
- time.sleep(1)
- await asyncio.sleep(1)
- wait(1)
- asyncio.pause(1)
Answer: await asyncio.sleep(1). await asyncio.sleep(1) yields control; time.sleep(1) blocks the whole event loop.
What does asyncio.create_task(coro()) do?
- Schedules the coroutine to run concurrently in the background
- Runs the coroutine immediately and blocks
- Creates a new OS thread
- Defines a new coroutine function
Answer: Schedules the coroutine to run concurrently in the background. create_task schedules the coroutine on the event loop so it runs concurrently with other code.
How do you offload a blocking CPU-heavy function without freezing the event loop?
- Call it directly inside the coroutine
- Wrap it in time.sleep()
- await asyncio.to_thread(func)
- Use await func()
Answer: await asyncio.to_thread(func). asyncio.to_thread runs blocking work in a thread so the event loop stays responsive.
What protocol methods must an async context manager (async with) implement?
- __enter__ and __exit__
- __aenter__ and __aexit__
- __next__ and __iter__
- __call__ only
Answer: __aenter__ and __aexit__. Async context managers define __aenter__ and __aexit__, used by async with.
Which exception should a task catch to handle cancellation gracefully?
- asyncio.TimeoutError
- KeyboardInterrupt
- StopAsyncIteration
- asyncio.CancelledError
Answer: asyncio.CancelledError. task.cancel() raises asyncio.CancelledError inside the task, which should be handled for clean shutdown.
AsyncIO is best suited for which kind of workload?
- CPU-bound work like math and ML training
- IO-bound work like network and file operations
- Heavy image processing
- Cryptographic hashing
Answer: IO-bound work like network and file operations. AsyncIO gives concurrency for IO-bound work; CPU-bound work needs multiprocessing.
What does passing return_exceptions=True to asyncio.gather do?
- Cancels all tasks on the first error
- Retries failed coroutines automatically
- Returns exceptions as results instead of raising them
- Disables exception handling entirely
Answer: Returns exceptions as results instead of raising them. With return_exceptions=True, gather collects exceptions into the results list instead of raising (fail-safe).
Continue this course
- Previous: Generators & Iterators Mastery
- Next: AsyncIO Deep Dive