Graceful shutdown with epoll/kqueue in Go: main loop stuck in Wait() (epoll_wait)
07:11 10 Jan 2026

I’m implementing a single-threaded event-driven server in Go using an OS-specific multiplexer abstraction (epoll on Linux, kqueue on macOS).
My main loop looks roughly like this:

sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGTERM, syscall.SIGINT)

for {
    select {
    case <-sigs:
        return //I have defer function for gracefully shutdown
    default:
        readyEvents, err := mp.Wait() // epoll_wait / kevent
        if err != nil {
            continue
        }

        for _, ev := range readyEvents {
            // accept/read/write logic
        }
    }
}

Using select with sigs doesn’t help because the blocking happens in the default case. The iteration can’t complete to check the context again.

So my questions are:

  1. What is the idiomatic way to unblock epoll_wait / kevent so a graceful shutdown can occur?

  2. How do production servers usually handle graceful shutdown in an event-loop model like this?

go sockets epoll kqueue