Python
asyncio
threading
epoll
concurrency

Can not join ev loop thread since it is hanging in epoll_wait

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

If an event loop thread is sitting in epoll_wait, that does not automatically mean it is broken. On Linux, that is often the normal idle state of a loop waiting for file-descriptor activity. The real reason join() hangs is usually that the loop was never told to stop, or it was told in a way that did not wake the loop thread safely.

Why epoll_wait Appears in Stack Traces

Event loops spend much of their life waiting for I/O readiness. Under the hood, that wait often maps to epoll_wait on Linux. So a blocked stack frame there usually means:

  • the loop is alive
  • it is waiting for work
  • no stop signal has completed the shutdown path yet

That is a different problem from a deadlock. An idle event loop is expected to sleep until new work or a wake-up event arrives.

join() Does Not Stop a Thread

This is the key mistake. thread.join() only waits for the thread to finish. It does not request shutdown.

If your code looks like this, it can wait forever:

python
thread.join()

The event loop thread must first receive a stop request and a wake-up event so it can exit its run loop.

Use a Thread-Safe Stop Path

For an asyncio loop running in another thread, the normal pattern is:

python
1import asyncio
2import threading
3
4
5def run_loop(loop):
6    asyncio.set_event_loop(loop)
7    loop.run_forever()
8    loop.close()
9
10
11loop = asyncio.new_event_loop()
12thread = threading.Thread(target=run_loop, args=(loop,))
13thread.start()
14
15loop.call_soon_threadsafe(loop.stop)
16thread.join()

call_soon_threadsafe matters because it schedules the stop request from another thread and wakes the loop correctly. That gives the loop a chance to leave run_forever, clean up, and let join() complete.

Clean Up Pending Tasks Before Closing

A second shutdown issue comes from pending tasks that keep the loop busy or leave resources half-open. A more complete pattern is:

python
1import asyncio
2import threading
3
4
5def run_loop(loop):
6    asyncio.set_event_loop(loop)
7    try:
8        loop.run_forever()
9    finally:
10        pending = asyncio.all_tasks(loop)
11        for task in pending:
12            task.cancel()
13        loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
14        loop.close()

This is useful when your loop owns long-lived coroutines. The stop request exits the main wait, and the cleanup phase drains outstanding work.

Why the Loop May Not Wake Up

If you modify shared state from another thread without using the event loop's thread-safe APIs, the loop can remain parked in epoll_wait because nothing notified it that shutdown work exists.

That is why direct cross-thread mutation is risky. The event loop needs a wake-up mechanism, not just a boolean flag in memory.

Debug the Shutdown Contract, Not Only the Stack Frame

When shutdown hangs, ask:

  • who owns the loop
  • who requests stop
  • is the stop request thread-safe
  • are there pending tasks or open transports
  • does the thread exit after the loop stops

These questions usually find the issue faster than treating epoll_wait itself as the bug.

Common Pitfalls

  • Calling join() and expecting it to stop the event loop thread.
  • Setting a shared flag from another thread without waking the loop.
  • Closing the loop before pending tasks have been cancelled or awaited.
  • Interpreting an idle epoll_wait frame as proof of a deadlock.
  • Mixing thread ownership rules and calling loop methods from the wrong thread.

Summary

  • 'epoll_wait is often the normal idle state of an event loop on Linux.'
  • A hanging join() usually means the loop was never stopped correctly.
  • Use thread-safe APIs such as loop.call_soon_threadsafe(loop.stop) to request shutdown.
  • Clean up pending tasks before closing the loop.
  • Treat loop shutdown as a protocol with wake-up, stop, cleanup, and then join.

Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Basis Lab
No course covers this one yet

Basis Lab writes one for you from a sentence about what you want to be able to do, then teaches it and checks you understood. Your first course is free.

Build my course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions