# event-listener: a condition variable you can await or block on

> event-listener is a small Rust primitive for notifying asynchronous tasks or threads, related to Dmitry Vyukov's eventcounts, and it exists to let you build a non-blocking data structure with both an async and a blocking interface. It suits anyone writing their own mutex or queue in Rust, and its no_std story depends on which feature you enable.

**smol-rs/event-listener** — Notify async tasks or threads

- Repository: https://github.com/smol-rs/event-listener
- Stars: 519 · Forks: 35
- Language: Rust
- License: Apache-2.0
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/smol-rs-event-listener

## What the primitive is for

event-listener notifies async tasks or threads. That is the whole description in the package metadata, and the README expands it into something more useful: this is a synchronization primitive similar to eventcounts, invented by Dmitry Vyukov.

The use case is stated plainly. You can use this crate to turn non-blocking data structures into async or blocking data structures, and the README points at a simple mutex implementation that exposes an async and a blocking interface for acquiring locks.

That framing matters, because it puts the crate in a specific place. It is not a general purpose condition variable, it is the primitive that a condition variable is built out of. When you have a queue, a channel or a lock of your own design and you need waiters to sleep until a state change, you need something that can hand out a listener and be told later that something happened. This crate is that something.

The design is the same idea as a condition variable with a counter, where a notifier increments a count and a waiter that sees the count has changed does not sleep. Vyukov's eventcounts article is the reference the README points to, and the crate's keywords in the manifest are `condvar`, `eventcount`, `wake`, `blocking` and `park`, with the categories `asynchronous` and `concurrency`.

It is a mature, small crate rather than a large framework. The version is 5.4.2, the edition is 2021, the minimum Rust version is 1.60, and the licence is the dual `Apache-2.0 OR MIT` with both files in the repository. The authors are Stjepan Glavina and John Nunley, and the crate lives in the smol-rs organisation alongside the other small async building blocks.

## The example shows the two-step check that makes it correct

The README's example waits until another thread sets a boolean flag, and the structure of the code is the lesson rather than the flag.

A thread is spawned that sleeps for a second, sets the flag, and then notifies all listeners:

```rust
        // Set the flag.
        flag.store(true, Ordering::SeqCst);

        // Notify all listeners that the flag has been set.
        event.notify(usize::MAX);
```

The waiter side is a loop with a check, a listen, a second check, and a wait:

```rust
    // Start listening for events.
    let listener = event.listen();

    // Check the flag again after creating the listener.
    if flag.load(Ordering::SeqCst) {
        break;
    }

    // Wait for a notification and continue the loop.
    listener.wait();
```

The comment in the source explains why the flag is checked twice. The first check is the fast path. The second check, after the listener has been created, closes the window in which a notification could arrive between the check and the start of listening. Without it, a notifier that fires in that gap would leave the waiter sleeping until the next event, which is the classic lost-wakeup bug.

The listener is created before the second check, and the wait happens on the listener, so the sequence is: check, subscribe, check again, sleep. That ordering is the contract, and it is the part of the API that a user has to get right.

The `event.notify(usize::MAX)` call is the other half of the contract. The argument is the number of listeners to wake, and `usize::MAX` means all of them. The crate is therefore a notify-many primitive rather than a signal-one, which is what makes it usable as the base of a condition variable, where every waiter is waiting for the same state change and any of them may be able to proceed.

## Features: std, critical-section and portable-atomic, and the spinlock warning

The features section is short and one warning in it matters more than the rest.

The `std` feature, enabled by default, enables the use of the Rust standard library, and disabling it gives you `no_std` support. The `critical-section` feature enables usage of the critical-section crate to enable a more efficient implementation of event-listener for `no_std` platforms. The `portable-atomic` feature enables the use of the portable-atomic crate to provide atomic operations on platforms that don't support them.

Then the warning. In production environments, at least one of `std` or `critical-section` should be enabled. This ensures that the internal locking mechanism has a critical section of some kind to fall back on. Otherwise, it falls back to a spinlock implementation, and that implementation is described as dangerous to rely on, with a link to a post on spinlocks considered harmful.

The mechanism behind the warning is the internal lock. The crate needs a mutual exclusion primitive to protect its listener list, and on `std` that is the parking crate, which is enabled by the `std` feature through `std = ["parking"]`. On `no_std` there is no parking lot, so the crate either uses a `critical-section` implementation, which is what the critical-section feature is for, or it spins. And a spinlock in a `no_std` context, on a single-core target where a spinning thread prevents the holder from making progress, is a deadlock rather than a slow path.

So the feature selection is not a preference. Enabling `std` gives you the parking-based lock, enabling `critical-section` gives you a platform-provided critical section, and enabling neither leaves you with the fallback the README tells you not to rely on.

There is also a `loom` feature, which maps to loom's parking and loom itself, and the manifest has a `[target.'cfg(loom)'.dependencies]` section for it, with a `check-cfg` lint entry for `cfg(loom)`. That is the concurrency model checker, and a crate that supports it is a crate whose internal locking has been verified under exhaustive scheduling.

## The manifest shows a crate designed for constrained targets

The dependency list in `Cargo.toml` is short enough to read in one pass, and it tells you what the crate is willing to require.

`critical-section` at 1.2.0 with `default-features = false` is optional. `pin-project-lite` at 0.2.12 is a hard dependency, which is how the crate keeps listener futures pinned correctly across await points. `portable-atomic-util` at 0.2.0 with the `alloc` feature is optional, and there is a second optional dependency under `[dependencies.portable_atomic_crate]` that renames the portable-atomic package to 1.2.0 with `default-features = false`, which is how the atomic operations are made available on platforms whose targets lack native atomics.

The `parking` crate at 2.0.0 is optional and scoped to `[target.'cfg(not(target_family = "wasm"))'.dependencies]`, so on WebAssembly the parking-based lock is not used at all. That exclusion is the reason the `wasm-bindgen-test` dev dependency exists under `[target.'cfg(target_family = "wasm")'.dev-dependencies]`, so the test suite runs in a browser as well as natively.

The dev dependencies describe the test surface. `futures-lite` at 2.0.0 provides the async executor for the tests, `try-lock` at 0.2.5 is a comparison implementation to check the mutex example against, `waker-fn` at 1 lets the tests drive a waker by hand, and criterion at 0.8 with `cargo_bench_support` drives the benchmark, with a `[[bench]]` entry named `bench` using `harness = false` and a `[lib]` section setting `bench = false` so the library itself is not benchmarked.

There is a `benches/` directory at the top level and an `examples/` directory containing `mutex.rs`, which the README links as the simple mutex implementation that exposes an async and a blocking interface for acquiring locks. The `tests/` directory and a `CHANGELOG.md` sit alongside.

## Where event-listener is the wrong choice

The first wrong-tool case is a mutex. The standard library has one, and it is well tested and well documented. The `examples/mutex.rs` file in this repository exists to show that a simple mutex is straightforward to build on event-listener, and that is a demonstration of the primitive's flexibility rather than a recommendation to replace `std::sync::Mutex`. If your synchronisation need is a lock, use a lock.

The second is a task wake-up mechanism in a single-threaded async runtime. If all your tasks run on one thread, the executor's own wakeup mechanisms are usually enough, and a crate designed to park OS threads may be more than you need.

The third is a `no_std` target where you have not thought about the critical section. The README is direct that the spinlock fallback is dangerous, and a design that depends on a lock you have not provided is a design that will hang on a single-core board under load. If you are going `no_std`, enable `critical-section` and make sure your platform implements it.

The fourth is a project that needs a broadcast of data rather than a signal of change. The primitive notifies that something happened, it does not carry a payload. A channel is the right tool when the waiter needs the value.

The alternative worth naming is a standard library condition variable, and the difference in approach is that a condition variable is tied to a single mutex and works with blocking waits, whereas this crate gives you the listener abstraction and lets you choose whether each waiter awaits or blocks. The trade is the reverse one too: you get a notification count rather than a single state, so the counting logic that a condition variable handles for you is yours to write, which is exactly what the double-check pattern in the example is doing.

## Maintenance state, licensing and what to check

The repository is not archived, and the last push to the default branch was on 2026-07-27, the same day as the v5.4.2 release. The release history shows v5.4.2 on 2026-07-27, v5.4.1 on 2025-08-03 and v5.4.0 on 2025-01-08.

So the pattern is one or two patch releases a year on a stable 5.x line, which is what a widely depended-on low-level primitive should look like. The manifest also carries a comment for the maintainers, saying that when publishing a new version you should update CHANGELOG.md and create a v5.x.y git tag, so the release procedure is written down in the place a contributor will look.

The licence is the dual option. You may use it under either the Apache License, Version 2.0, or the MIT license, at your option, and both licence files are in the repository. The contribution section states that unless you explicitly say otherwise, any contribution intentionally submitted for inclusion is dual licensed the same way, without additional terms. That is a permissive and unremarkable arrangement, and it means a consuming project can pick whichever licence its own policy prefers. This is a description of the published terms and not legal advice.

The minimum supported Rust version is 1.60, which is a notably old floor and a good sign for a crate meant to sit under everything else. The `exclude = ["/.*"]` entry in the manifest keeps dotfiles out of the published package.

For an adopter, the check that matters is the feature set, and the README already tells you what it is. Decide between `std` and `critical-section` first, and if you are on WebAssembly, know that the parking-based lock is not compiled in and the platform has to supply the critical section. Then read `examples/mutex.rs`, because it is the shortest complete example of the whole API and it shows the ordering that a user has to reproduce.

## Conclusion

Adopt event-listener when you are writing a lock-free or partially blocking data structure in Rust and need waiters to be woken by a notifier without a spin loop, because the crate gives you a listener that can be awaited or blocked on, and the same primitive works for both. Do not adopt it to replace a mutex you could use from the standard library, since the `examples/mutex.rs` implementation exists precisely to show that a simple mutex is easy to build on it. Before you depend on it, decide which feature set you need, because the README is explicit that in production at least one of `std` or `critical-section` should be enabled, and the fallback spinlock implementation is described as dangerous to rely on.

## FAQ

### What does the event-listener crate provide?

It provides a synchronization primitive for notifying async tasks or threads, similar to eventcounts invented by Dmitry Vyukov. The README says you can use it to turn non-blocking data structures into async or blocking data structures, and points at a simple mutex implementation exposing both interfaces.

### How do I use event-listener correctly in Rust?

Check your condition, create a listener with `event.listen()`, check the condition again to close the window between the check and the subscription, then wait on the listener. The README's example uses that exact ordering with an atomic flag, and the notifier side calls `event.notify(usize::MAX)` to wake all listeners.

### Can event-listener be used without the standard library?

Yes. Disabling the default `std` feature gives you `no_std` support, and the `critical-section` feature enables a more efficient implementation on `no_std` platforms. The README warns that at least one of `std` or `critical-section` should be enabled in production, because otherwise the crate falls back to a spinlock implementation that is described as dangerous to rely on.

### What other feature flags does event-listener have?

The `portable-atomic` feature pulls in the portable-atomic crate to provide atomic operations on platforms that do not support them, and there is a `loom` feature that maps the parking lock and the loom crate so the internal locking can be checked with a concurrency model checker. The default feature set is just `std`, which enables parking.

### What licence is event-listener released under?

Either the Apache License, Version 2.0 or the MIT license, at your option, with `LICENSE-APACHE` and `LICENSE-MIT` both in the repository. The contribution terms state that contributions are dual licensed the same way unless you explicitly say otherwise.

## Sources

- [Issues](https://github.com/smol-rs/event-listener/issues)
- [License: Apache-2.0](https://github.com/smol-rs/event-listener/blob/master/LICENSE)
- [README](https://github.com/smol-rs/event-listener/blob/master/README.md)
- [Releases](https://github.com/smol-rs/event-listener/releases)
- [smol-rs/event-listener on GitHub](https://github.com/smol-rs/event-listener)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/smol-rs-event-listener
