Back to Blog
Python

Using Python asyncio Event for Coroutine Signaling

python asyncio event: Learn how to use asyncio.Event to coordinate coroutines, handle timeouts and cancellation, and avoid common pitfalls in Python async programs.

asyncioconcurrencyevent-drivencoroutinessynchronization
Abstract illustration of an asyncio event signaling between coroutines in Python

When you need one coroutine to notify others that a condition has occurred, python asyncio event provides a clean, built-in primitive. asyncio.Event lets you signal across tasks without manual condition variables or polling loops. This article explains how to use it correctly, what happens when you wait or set, and where it breaks down in production code.

What asyncio.Event Does and When to Use It

asyncio.Event is a synchronization primitive that allows one or more coroutines to wait until another coroutine sets the event. It is analogous to threading.Event but designed for the asyncio event loop. Internally, it maintains a boolean flag and a set of waiters. When the flag is set, all current and future waiters resume immediately. When cleared, new waiters block until the next set.

Use an event when you need to broadcast a single signal to multiple consumers, such as a shutdown notification, a resource readiness flag, or a state change that should unblock several tasks. It is not a queue; it carries no data beyond the boolean state. For passing actual messages, prefer asyncio.Queue.

Creating and Using an asyncio.Event

Creating an event is straightforward:

import asyncio async def main(): event = asyncio.Event() print(event.is_set()) # False

The event starts unset. To set it, call .set(). To clear it, call .clear(). To wait until it is set, call .wait().

import asyncio async def worker(event): print("worker waiting") await event.wait() print("worker proceeding") async def main(): event = asyncio.Event() task = asyncio.create_task(worker(event)) await asyncio.sleep(0.1) event.set() await task asyncio.run(main())

When .set() is called, all tasks that are currently blocked in await event.wait() are resumed in the same event loop iteration. If a task calls wait() after the event is set, it returns immediately without yielding control.

The .clear() method resets the flag to False. This is useful when the signal is one-shot and you want to reuse the event for a later cycle. Be careful: clearing while other tasks are still waiting will cause them to continue waiting, which may be unexpected if they were supposed to proceed once the event was set.

Waiting with Timeouts and Cancellation

asyncio.Event.wait() accepts an optional timeout argument. If the event is not set within the timeout, the wait raises asyncio.TimeoutError.

import asyncio async def main(): event = asyncio.Event() try: await asyncio.wait_for(event.wait(), timeout=1.0) except asyncio.TimeoutError: print("event not set within 1 second")

Using asyncio.wait_for is often clearer than passing a timeout to wait() directly, because it also works with other awaitables. When the timeout occurs, the wait is cancelled, and the coroutine raises TimeoutError.

Cancellation behaves like any other awaitable. If the task that called wait() is cancelled, the waiter is removed from the event's internal waiter set. This avoids memory leaks in long-running applications.

Common Signaling Patterns

A typical pattern is a graceful shutdown signal. Multiple worker tasks wait on an event, and a main task sets it when it's time to stop.

import asyncio async def worker(name, shutdown_event): while not shutdown_event.is_set(): print(f"{name} working") try: await asyncio.wait_for(shutdown_event.wait(), timeout=1.0) except asyncio.TimeoutError: continue print(f"{name} shutting down") async def main(): shutdown = asyncio.Event() workers = [asyncio.create_task(worker(f"w{i}", shutdown)) for i in range(3)] await asyncio.sleep(2) shutdown.set() await asyncio.gather(*workers) asyncio.run(main())

Another pattern is using an event to signal that a resource is ready. For example, a connection pool initializer sets an event after the pool is populated, and request handlers wait on it before using the pool.

You can also combine events with asyncio.gather to wait for multiple signals. However, events do not carry information about which signal fired. If you need to distinguish between multiple sources, use separate events and asyncio.wait with return_when='FIRST_COMPLETED'.

asyncio.Event vs threading.Event

Both primitives share the same API: set(), clear(), wait(), and is_set(). The critical difference is that asyncio.Event.wait() is a coroutine and must be awaited, while threading.Event.wait() is a blocking method. Using threading.Event inside an async function blocks the event loop and defeats the purpose of concurrency.

Aspectasyncio.Eventthreading.Event
Wait methodawait event.wait()event.wait()
BlockingNon-blocking, yields controlBlocks the calling thread
Thread safetyNot thread-safeThread-safe
Use caseWithin asyncio tasksAcross OS threads

asyncio.Event is not thread-safe. If you need to set an event from a different thread, use loop.call_soon_threadsafe(event.set) to marshal the call onto the event loop. This is a common source of bugs in mixed-thread applications.

Pitfalls and Edge Cases

One common mistake is calling clear() immediately after set() in the same task. Because set() schedules all waiters to resume, clearing right away can cause waiters that haven't yet run to see the event as unset. This leads to lost wake-ups. If you need a one-shot signal, do not clear; create a new event instead.

Another pitfall is relying on is_set() for state checks in a race. The flag can change between the check and the next await. Use await event.wait() rather than if event.is_set(): when you need to block until the signal occurs.

Also, be aware that asyncio.Event does not support multiple independent

python asyncio event: Practical Usage and Code Examples | RYUSLOG DEV