polling: one readiness API over epoll, kqueue, event ports and IOCP
Portable interface to epoll, kqueue, event ports, and wepoll
At a glance
- What is it?
- polling is a small Rust crate that gives a single interface to the readiness notification mechanisms of Linux, the BSDs, macOS, the Solaris family and Windows, with oneshot, level triggered and edge triggered modes. It is the layer underneath crates such as mio, and it comes with a hard constraint: only one thread may wait for events at a time.
- Who is it for?
- polling suits a Rust developer writing an event loop of their own, or wrapping one, who needs the same code to run on Linux, macOS, the BSDs and Windows without conditional compilation at every call site, and who is willing to own the semantics of re-arming interest.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 11 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Four kernel mechanisms, one API
polling describes itself as a portable interface to epoll, kqueue, event ports and IOCP, and the repository description words it slightly differently, mentioning a polling abstraction as well. Either way, the shape of the project is a thin, uniform layer over the readiness notification mechanisms that operating systems expose.
Those mechanisms are genuinely different from each other. Linux has epoll, the BSD family and macOS have kqueue, the Solaris family has event ports, Windows has the I/O completion port, and a long tail of Unix-likes has nothing but the plain poll call. Writing a network application that runs everywhere means writing that layer, and writing it well means getting the re-arming semantics right on each platform.
That is the job this crate takes on. The Rust keywords listed in the manifest are the honest summary of the audience: the crate is used by projects that need an event loop, and the keywords include the name of the crate that most famously provides one on top of it.
The design goal is visible in the API. You create a poller, you add a source with a set of events, you wait for events into a vector, and you read the keys you attached to them. There is no callback registration, no thread pool, no reactor loop: the crate hands you the readiness notifications and lets you write the loop, which is what makes it usable as the bottom layer of something larger.
Which systems get which mechanism
The README lists the supported platforms by mechanism, and the list is longer than most people expect.
The epoll backend covers Linux, Android and RedoxOS. The kqueue backend covers macOS, iOS, tvOS, watchOS and visionOS, plus FreeBSD, NetBSD, OpenBSD and DragonFly BSD.
The event ports backend covers illumos and Solaris. The plain poll backend covers VxWorks, Fuchsia, HermitOS and other Unix systems. And the IOCP backend covers Windows, and Wine at version 7.13 or later.
Two entries are worth pausing on. Fuchsia and HermitOS are operating systems where the conventional Unix event mechanisms are not the natural fit, and their presence says that the crate is aimed at portable software rather than at servers. And the Wine version requirement is the kind of detail that saves an afternoon when a build fails on a machine without a native Windows kernel.
The manifest shows how the platform support is implemented, and the answer is target specific dependencies rather than conditional compilation. Unix, Fuchsia and VxWorks targets depend on the rustix crate with the event, file system, pipe, process, standard library and time features. Windows targets depend on a concurrent queue, a projection library and the raw system bindings. Hermit has its own ABI crate. And the platform selection itself is done with the cfg-if crate, which is a tidy way to keep the conditional compilation in one place.
Oneshot by default, and the re-arming rule
The most important line in the README is the one about the default mode.
By default, polling is done in oneshot mode, which means interest in I/O events needs to be re-enabled after an event is delivered if you are interested in the next event of the same kind.
That single sentence is the difference between a working event loop and a program that handles one connection and then goes quiet. In oneshot mode, a source that has delivered a readable event is no longer registered for readability, so the next connection will never wake you up unless you ask for it again.
The mechanism for asking again is the modify operation, and the README's example does exactly that after accepting a connection.
Level and edge triggered modes are also available, but the README qualifies them: only for certain operating systems. So the modes you would prefer are not uniformly portable, which is why oneshot is the default, because it is the behaviour you can rely on everywhere.
The modes are documented on the PollMode type in the crate documentation, and the crate documentation is on docs.rs with the badge in the readme.
One waiter, and why
There is a constraint stated as its own line in the README, between the mode explanation and the examples.
Only one thread can be waiting for I/O events at a time.
That is a restriction of the underlying mechanisms rather than of the crate's design choices. The readiness notification model on these platforms is built around a single waiting thread per poller: the kernel wakes the waiter, the waiter reads the notifications and re-registers interest. Two threads waiting on one poller would race to consume the same notifications.
The practical consequence for an application design is that the polling thread is the one that waits, and the work it discovers has to be handed to other threads if you want concurrency. That is the shape of every reactor that uses this crate: one event loop thread, a work queue, and worker threads that never touch the poller.
It also means the crate cannot be used as a general purpose thread waiting primitive. If you were hoping to have several threads each blocked on different sockets through a shared poller, that is not a supported pattern, and the README says so before you build it.
The example loop, and what it teaches
The README's example is a complete TCP accept loop, and it is worth reading as the specification of the API rather than as a tutorial.
use polling::{Event, Poller};
use std::net::TcpListener;
// Create a TCP listener.
let socket = TcpListener::bind("127.0.0.1:8000")?;
socket.set_nonblocking(true)?;
let key = 7; // Arbitrary key identifying the socket.
// Create a poller and register interest in readability on the socket.
let poller = Poller::new()?;
poller.add(&socket, Event::readable(key))?;
// The event loop.
let mut events = Vec::new();
loop {
// Wait for at least one I/O event.
events.clear();
poller.wait(&mut events, None)?;
for ev in &events {
if ev.key == key {
// Perform a non-blocking accept operation.
socket.accept()?;
// Set interest in the next readability event.
poller.modify(&socket, Event::readable(key))?;
}
}
}Five things are visible in it. The socket is set to non-blocking mode, because the accept is expected not to block once readiness has been signalled. The key is an arbitrary number you attach to the registration, which is how you identify which source an event belongs to, since the interface does not hand back the source itself. The events vector is cleared and reused across iterations rather than allocated each time. The wait is called with no timeout, so the loop blocks until something happens.
And the modify call at the end of the handler is the oneshot rule in action. That single line is the difference between a server that keeps accepting and one that accepts exactly one connection.
How the crate is built and tested
The manifest and the repository layout describe a project with a strong emphasis on testing across platforms.
The crate has no mandatory dependencies beyond a conditional compilation helper, and the optional ones are few: tracing for diagnostics, which can be switched off entirely with the default features disabled.
The testing setup is more revealing. The lint configuration declares the expected configuration flags explicitly, with a check for the polling test poll backend and the polling test epoll pipe, which means the test suite can exercise the alternative backends deliberately rather than only the default one on the host system. The development dependencies include a parallel test runner and a random number generator, plus the socket extension crate and a signal handling library for the Unix targets.
The repository layout supports the same aim. There is a continuous integration configuration for the Cirrus CI service, which is the one that runs on the non-Linux Unix systems, alongside the GitHub workflow for the main matrix. There is a cross-compilation configuration file for building the crate for targets you do not have hardware for. And the examples directory contains three programs: a TCP client, a case with two listeners, and one that waits on a signal, which is the interesting one, because waiting for a signal through a readiness API is exactly the kind of thing that is easy to get wrong on a platform whose signal handling differs.
Versions, licence and where it fits
The releases are v3.11.0 from 2025-09-14, v3.10.0 from 2025-08-03 and v3.9.0 from 2025-07-19, and the last push was on 2026-10-12.
The version number is the signal here. The crate is on a 3.x line, so the API is stable in the Rust sense, and the releases are small and frequent, which is what a portability layer should look like when the platforms underneath it keep moving.
The manifest gives the compatibility floor directly: edition 2021 and a minimum Rust version of 1.71. It also records the release procedure in a comment, that a new version means updating the changelog and creating a version tag, and the crate excludes dot files from the published package.
On the licence, the crate is offered under the Apache Licence 2.0 or the MIT licence at your option, and the contribution section adds the usual clause that contributions are dual licensed under the same terms unless the contributor states otherwise.
So the question is where this crate fits. It is a mechanism, not a runtime: you write the loop, you own the re-arming, you own the single waiter rule, and you get the same code on every platform. If you want an event loop, use one of the crates built on it. If you are writing your own runtime, or you need readiness notifications inside something that already has its own loop, this is the layer to build on.
Editorial conclusion
polling suits a Rust developer writing an event loop of their own, or wrapping one, who needs the same code to run on Linux, macOS, the BSDs and Windows without conditional compilation at every call site, and who is willing to own the semantics of re-arming interest. It is the wrong choice if you want a runtime, since this is a mechanism rather than a framework, and the wrong choice if your design needs several threads waiting on the same sources, which the single waiter rule forbids. Verify first which poll modes your target systems actually support, since level and edge triggered operation is only available on certain operating systems, and read the oneshot default carefully before you write the loop, since forgetting to re-enable interest is the classic bug with this API.
Frequently asked questions
What does the polling crate provide?
It provides a portable interface to the readiness notification mechanisms of operating systems: epoll on Linux, Android and RedoxOS, kqueue on macOS, the BSDs and the Apple platforms, event ports on illumos and Solaris, the plain poll call on other Unix systems and IOCP on Windows.
What does oneshot mode mean?
In the default oneshot mode, interest in I/O events has to be re-enabled after an event is delivered if you want the next event of the same kind, which is why the example calls the modify operation after each accept. Level and edge triggered modes also exist but only on certain operating systems.
Can several threads wait on the same poller?
No. The README states that only one thread can be waiting for I/O events at a time, so the polling thread has to be the one that waits and any work it discovers has to be handed to other threads.
Under which licence is polling released?
Under the Apache Licence 2.0 or the MIT licence, at your option, and contributions are dual licensed the same way unless the contributor states otherwise.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/smol-rs-polling)Was this useful?