2025, Oct 06 21:00
Reliable shutdown of an asyncio TCP server using multiprocessing.Event: avoid polling with run_in_executor or bridge to an asyncio.Event
Learn how to stop an asyncio TCP server via multiprocessing.Event without flaky polling. Use run_in_executor or bridge to an asyncio.Event for deterministic shutdowns.
Stopping an asyncio-based server from another process sounds straightforward with multiprocessing.Event, but mixing cross-process synchronization and an async event loop can turn into an unreliable polling loop. The core task is to react to a termination signal without stalling the loop or depending on timing-sensitive checks.
Problem statement
Consider an async TCP server that periodically polls a multiprocessing.Event to decide when to exit:
self._stop_signal = multiprocessing.Event()
tcp_srv = await asyncio.start_server(
    self.on_client,
    self.bind_host,
    self.bind_port,
    backlog=self.backlog_size
)
async with tcp_srv:
    while not self._stop_signal.is_set():
        await asyncio.sleep(0.01)
The expectation is that another process calls set() on the event, the loop observes it, and the server shuts down. In practice, this polling can be flaky: sometimes it exits, sometimes it doesn’t.
What’s going on
Polling a multiprocessing.Event with sleeps inside an asyncio loop is inherently timing-sensitive. The loop wakes up on a schedule and checks the flag, which invites race conditions. More importantly, the general rule is to avoid invoking blocking multiprocessing primitives directly in asyncio. Offloading the wait to a thread or bridging to an asyncio.Event is a safer approach than repeatedly checking state.
There is an additional nuance. The simple flag check itself is synchronous and non-blocking, but relying on a tight poll with sleep is still brittle for termination signaling. A dedicated wait that doesn’t block the event loop is the cleaner design.
A robust approach
Run the blocking wait in a thread, and await it from asyncio. This isolates the process-level synchronization away from the event loop and removes the need for ad hoc polling.
import asyncio
import multiprocessing
async def await_shutdown(stop_evt):
    loop = asyncio.get_running_loop()
    await loop.run_in_executor(None, stop_evt.wait)
# In the server task:
async with tcp_srv:
    shutdown_task = asyncio.create_task(
        await_shutdown(self._stop_signal)
    )
    await shutdown_task
This pattern ensures the event loop stays responsive while a thread waits on the process-level event. Once the event is set, the task resolves and you can proceed with shutdown logic.
Alternative: bridge to an asyncio.Event
Another option is to bridge the multiprocessing.Event to an asyncio.Event using a background thread. The thread blocks on the process event, then signals the async one in a loop-safe way.
class EventRelay:
    def __init__(self, proc_evt):
        self.proc_evt = proc_evt
        self.aio_evt = asyncio.Event()
    def launch_watch(self):
        def watcher():
            self.proc_evt.wait()
            asyncio.run_coroutine_threadsafe(
                self._notify_aio(), asyncio.get_running_loop()
            )
        threading.Thread(target=watcher, daemon=True).start()
    async def wait(self):
        await self.aio_evt.wait()
This maintains the separation between blocking synchronization and the async runtime, while letting the rest of the system use an asyncio-native primitive.
Why this matters
Async servers rely on a responsive event loop. Injecting blocking calls or depending on polling loops undermines that. Isolating the wait into a thread or bridging to asyncio primitives keeps your loop healthy and makes shutdowns deterministic, without relying on arbitrary sleep intervals.
Conclusion
If you need to stop asyncio code based on a multiprocessing.Event, don’t poll it inside the loop. Either run the event’s wait() in a thread via run_in_executor (or an equivalent helper), or bridge it to an asyncio.Event using a background thread. Both approaches avoid blocking the loop and remove timing flakiness from shutdown control.
The article is based on a question from StackOverflow by UnemployedBrat and an answer by Mahrez BenHamad.