async-channel: a work queue where each message goes to exactly one consumer
Async multi-producer multi-consumer channel
At a glance
- What is it?
- async-channel is a small Rust crate providing an async multi-producer multi-consumer channel, bounded or unbounded, with cloneable senders and receivers and explicit close semantics. It fits Rust programs that need to hand work between tasks without a blocking send.
- Who is it for?
- Adopt async-channel if you are writing Rust with async tasks and need a bounded or unbounded work queue where each message is delivered to one consumer, especially if you care about a minimum supported Rust version of 1.60 or a build without the standard library.
- 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 102 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
Each message is received by exactly one of the consumers
The defining property of this crate is in its opening line: an async multi-producer multi-consumer channel, where each message can be received by only one of all existing consumers.
That last clause is the whole design. Multiple producers can send, multiple consumers can receive, and a message goes to one consumer. The channel behaves as a work queue rather than a broadcast, which is the right shape when the messages are tasks to be done once each, and the wrong shape when they are state changes that every participant needs to observe.
Two kinds of channel exist. The bounded channel has limited capacity, and the unbounded channel has unlimited capacity. The bounded variant gives backpressure, because a send on a full channel waits for a receiver to make room, and that is the mechanism that keeps a producer from outrunning a consumer. The unbounded variant never blocks on capacity, and its cost is memory that grows with whatever is queued and never consumed.
The channel has a Sender side and a Receiver side, and both are cloneable and can be shared among multiple threads. Cloning is how you fan out: hand a clone of the sender to each producer, a clone of the receiver to each consumer, and the channel routes messages among them.
The crate is small on purpose. It is a channel and nothing else, which is why it can be reasoned about in a sitting.
Closing drains the queue instead of discarding it
The close semantics are described precisely, and they are the part most worth reading before you rely on the channel to signal shutdown.
When all Senders or all Receivers are dropped, the channel becomes closed. When a channel is closed, no more messages can be sent, but remaining messages can still be received. So a closed channel is not an empty channel. It is a channel where the producers are gone and the backlog is still deliverable, and a consumer that is draining will keep getting messages until the queue runs dry, then observe the end.
The channel can also be closed manually, by calling Sender::close() or Receiver::close(). Either side can shut the door, which is what makes the two idioms distinguishable. A receiver calling close is saying, I have seen enough, senders should stop. A sender calling close is saying, no more work is coming, and it does so while the sender itself can still be alive and still hold the queue open for receiving.
The pattern that falls out of this is a graceful shutdown. A producer that finishes its work drops its sender, or calls close, and the consumers keep working through whatever is already queued instead of losing it. That is a stronger guarantee than a channel that discards pending items on close, and it is the reason the drop-the-last-sender rule is documented so precisely.
The implementation rests on a concurrent queue and a listener strategy
The Cargo.toml explains how the crate is put together, and the dependency list is short enough to be informative.
concurrent-queue at version 2.5 is the storage, and it is pulled with default-features = false. event-listener-strategy at 0.5.4 handles the wake-up side, the machinery that knows which task to notify when a message arrives or space frees up, and it too is pulled without default features. futures-core at 0.3.5 provides the future types, and pin-project-lite at 0.2.11 is the pinning helper, which is what allows the futures in this API to be stack-pinned and self-referential without a heavier pinning crate.
Two optional dependencies, portable-atomic and portable-atomic-util, sit behind the portable-atomic feature, and both are enabled with the require-cas and alloc features respectively. That feature name is the giveaway: it exists so the same crate can build for targets whose atomics are not natively available, by pulling in a portable implementation of compare-and-swap. The feature list wires it through to both concurrent-queue and event-listener-strategy, so the whole stack switches together rather than one half of it.
The features section shows a std feature that is on by default and pulls the std variants of the two core dependencies. Combined with the default-features = false declarations, that is the arrangement a crate uses when it wants to offer a build without the standard library, and the std feature is how you turn that back on.
A minimum Rust version of 1.60 and an edition from 2021
The package metadata sets edition = "2021" and rust-version = "1.60". That is the declared minimum supported Rust version, and it is a meaningfully old floor for a crate published in the 2.x line, which tells you something about how carefully the project guards its toolchain requirements.
The keywords are mpmc, mpsc, spmc, chan and futures, and the categories are asynchronous and concurrency. The description in the manifest matches the README almost word for word, which is a small sign of a project that keeps its metadata honest.
The development dependencies show how the crate is tested. easy-parallel at version 3 and futures-lite at version 2 are the test-side tools, the first for running work in parallel and the second for a small executor to drive the futures. There is also a target-specific dev-dependency, wasm-bindgen-test at 0.3.37, declared for cfg(target_family = "wasm"), so the test suite can run under a wasm target through the wasm-bindgen test harness. That is a deliberate inclusion, since it means the channel's behaviour is exercised in a single-threaded environment as well as on a threaded one, and it is a plausible place to look for the kind of ordering difference that only appears when there is no real parallelism.
The repository layout is minimal by design: src/, tests/, Cargo.toml, CHANGELOG.md, README.md, both licence files, and a .github/ directory for the CI configuration referenced by the build badge. There is no examples/ directory and no bench directory in the top level, so the documentation surface is the README plus the API docs published on docs.rs.
Sending and receiving takes two awaited calls
The README's example is the entire quick start, and it is worth reading closely because it shows the shape of the API rather than a toy use of it.
let (s, r) = async_channel::unbounded();
assert_eq!(s.send("Hello").await, Ok(()));
assert_eq!(r.recv().await, Ok("Hello"));Creating a channel returns a tuple of the two ends. The unbounded constructor is used here; a bounded one would take a capacity. Sending awaits, and the result is a Result, so a send on a closed channel is an error rather than a panic, and receiving returns a Result too, which is how the end of the channel is observed rather than through a sentinel value.
Both calls are awaited, which is the reason this crate exists in its async form. A send on a full bounded channel does not block the thread, it parks the task until a receiver frees a slot, and a receive on an empty channel parks the task until a message arrives. The event-listener-strategy dependency is the machinery that makes that parking efficient, and the fact that it is a separate crate rather than something inlined is what allows the same waking behaviour to be reused by other projects in the ecosystem.
To depend on it, the manifest entry is the usual one, and the crate is published on crates.io under the name async-channel:
[package]
name = "async-channel"
version = "2.5.0"
edition = "2021"
rust-version = "1.60"The version there is the current one, 2.5.0, matching the release history.
Where a work queue is the wrong shape
The main failure mode of this crate is choosing it for a broadcast problem.
Because each message can be received by only one of the existing consumers, a state update sent over this channel reaches exactly one listener. If your design needs every participant to see every change, this channel cannot express that, and no amount of cloning the receiver fixes it: cloning adds more consumers to the same work queue, not more subscribers to a feed. The shape you want in that case is shared state behind a lock with a notification channel, where consumers read the current value rather than being handed a copy of each change.
The other structural limits are in the API and the release history. Everything is async, so using the channel from synchronous code means bridging it into an executor, and the crate offers no blocking variant. The queue itself has no persistence, no delivery acknowledgement and no redelivery, so a message consumed by a task that then fails is gone. A channel like this moves values between tasks; it is not a durable queue.
On maintenance, the facts are modest. The last push was on 2026-07-13, and the most recent release, v2.5.0, was published on 2025-07-06, after v2.4.0 three days earlier and v2.3.1 in May 2024. The repository is not archived, so the crate is not dead, but the gap between the newest tag and the last commit is long enough that you should read CHANGELOG.md before assuming a fix has shipped. The crate is also small enough that it is plausible to read src/ directly if you need certainty about a behaviour the README leaves open.
Dual licensed, with contributions under the same terms
The licence is the permissive kind. The README states the crate is licensed under either of the Apache License, Version 2.0, with the file LICENSE-APACHE in the repository root, or the MIT license, with LICENSE-MIT, at your option. The manifest records the same choice as Apache-2.0 OR MIT.
The choice between the two matters in one practical respect. Apache-2.0 includes an express patent grant and requires that modifications to the licensed files carry notices of changes, while MIT is shorter and carries no patent clause. A team with a corporate patent review process will often want Apache-2.0, and the ability to pick per project is exactly why a dual licence is offered.
The contribution clause is spelled out as well. Any contribution intentionally submitted for inclusion in the work, as defined in the Apache-2.0 license, shall be dual licensed as above, unless the contributor explicitly states otherwise. So incoming patches land under the same terms as the rest of the crate by default, and a contributor who wants different terms has to say so explicitly.
This is a factual reading of the files, not legal advice. The practical point for an adopter is simply that the terms are clear, permissive and identical to what most Rust infrastructure crates use, so the licence is unlikely to be the reason not to depend on it.
Editorial conclusion
Adopt async-channel if you are writing Rust with async tasks and need a bounded or unbounded work queue where each message is delivered to one consumer, especially if you care about a minimum supported Rust version of 1.60 or a build without the standard library. Do not adopt it if you need every consumer to see every message, since that is a broadcast shape this crate does not provide, and do not assume the release train is fast, because v2.5.0 shipped on 2025-07-06 and the last push was on 2026-07-13. Before pinning, read CHANGELOG.md and confirm the Cargo.toml rust-version of 1.60 is compatible with the toolchain your build enforces.
Frequently asked questions
What is async-channel used for in Rust?
It is an async multi-producer multi-consumer channel where each message is received by only one of the existing consumers, available as a bounded channel with limited capacity or an unbounded one. Both the Sender and Receiver sides are cloneable and can be shared among multiple threads.
How do you close an async-channel channel?
The channel closes automatically when all Senders or all Receivers are dropped, and it can also be closed manually by calling Sender::close() or Receiver::close(). Once closed no more messages can be sent, but messages already in the channel can still be received.
What is the minimum supported Rust version for async-channel?
The Cargo.toml sets rust-version = "1.60" with edition = "2021". The crate also offers a std feature that is enabled by default, alongside an optional portable-atomic feature for targets without native compare-and-swap support.
How do I add async-channel to a Rust project?
Add async-channel to your Cargo.toml dependencies, then create a channel with the unbounded or bounded constructor and await send and recv on the two ends. The current published version is 2.5.0 and the crate is available on crates.io.
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-async-channel)Was this useful?