kanal: a channel that works in both sync and async Rust
The fast sync and async channel that Rust deserves
At a glance
- What is it?
- Kanal is a Rust channel library inspired by the CSP model, and its distinguishing feature is that a single channel type can be used from synchronous code and asynchronous code, with as_async and to_sync converting between them. It ships MPMC and SPSC channels, a Go style close broadcast, and a loom model checking suite for the handoff paths, and it is still a beta version.
- Who is it for?
- Adopt kanal if your Rust code has both blocking and async parts that need to talk to each other, since that is the case it handles better than separate sync and async channel types. Do not adopt it in production without pinning to a released version, because the crate version in the repository is 0.2.0-beta1 while the README still tells you to depend on 0.1.
- Can I use it commercially?
- Yes. MIT 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 111 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 November 7, 2026, and from our analysis. They are not legal advice.
Star history
- fereidani/kanal1,769 ★ · +1
| Repository | 2026-10-16 | 2026-11-07 |
|---|---|---|
| fereidani/kanal | 1.8k | 1.8k |
1 Stars in the last 22 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
A channel inspired by CSP rather than by the standard library
Kanal is described as a Rust implementation of channels inspired by the CSP, Communicating Sequential Processes, model. The stated goal is to help programmers create efficient concurrent programs by providing multi producer and multi consumer channels with advanced features for fast communication, and the library focuses on unifying message passing between synchronous and asynchronous parts of Rust code, offering a combination of synchronous and asynchronous APIs while maintaining high performance.
The Rust standard library gives you std::sync::mpsc, which is synchronous only, and the async ecosystem gives you a separate channel type for the runtime. The unification is the point of this library. A channel created in one style can be used in the other, and the API for the conversion is a method call rather than a rewrite.
The crate is MIT licensed, the author is Khashayar Fereidani, and the docs are on docs.rs. The repository is not archived and the last push was on 2026-07-19, though there are no GitHub releases, so the crate version in Cargo.toml is the only version marker, and it reads 0.2.0-beta1.
That version string is worth a second look, because the README's install instruction still says to depend on the 0.1 line:
[dependencies]
kanal = "0.1"So the documentation and the source tree are describing different versions. The edition is 2021, the keywords are channel, mpsc, mpmc and async, and the feature list is small: an async feature that pulls in futures-core, a std-mutex feature, and default = ["async"].
The audience is a Rust developer who has hit the wall where a blocking producer and an async consumer have to meet, and who does not want two channel types and two error types as a result.
The composite transfer trick and the zero capacity channel
The README explains why Kanal is faster in three numbered points, and the first one is the most interesting because it is a property of the protocol rather than of the hardware.
Kanal employs a highly optimized composite technique for the transfer of objects. When the data size is less than or equal to the pointer size, it utilizes serialization, encoding the data as the pointer address. Conversely, when the data size exceeds the pointer size, the protocol employs a strategy similar to the one used by the Golang programming language, utilizing direct memory access to copy objects from the sender's stack or write directly to the receiver's stack.
So the transfer mechanism is chosen per message. A small message is not copied at all, it becomes the pointer value. A large message is moved rather than cloned, from one stack to the other, without an intermediate buffer. The stated benefit is that the method eliminates unnecessary pointer access and eliminates heap allocations for bounded(0) channels.
That last clause is the one with a practical consequence. A bounded channel with capacity zero is a rendezvous channel, the one where a send blocks until a receiver takes the value. In an implementation that copies through a queue, that channel needs a buffer for the handoff, which means a heap allocation on the path where the message is by definition never queued. Kanal avoids that allocation, and it is a property you can feel in a workload of small messages with a strict handoff.
The dependency list supports the reading that the transfer is hand written rather than delegated. The runtime dependencies are branches, cacheguard, lock_api and the optional futures-core. There is no async runtime in the dependency list at all, which is what lets the same channel work in a synchronous program with no executor present.
The tuned mutex and the std-mutex escape hatch
The second reason in the README is about locking.
Kanal utilizes a specially tuned mutex for its channel locking mechanism, made possible by the predictable internal lock time of the channel. The reasoning is that a custom lock can be specialised when the critical section has a known and bounded shape, which a general purpose mutex cannot assume.
And then there is the escape hatch, which is the most useful sentence in the section for an evaluator. It is possible to use the Rust standard mutex with the std-mutex feature, and Kanal will perform better than competitors with that feature too.
That is a checkable claim about the design rather than a marketing line. If the custom lock were the whole story, swapping in the standard mutex would cost a lot, and the result would be at best on par with other libraries. The claim is that the underlying channel design, the transfer protocol and the parking of waiting threads, is what carries the performance, and the lock is a detail on top.
The std-mutex feature is one of only three declared features, and it is empty in the manifest, std-mutex = [], which confirms it only switches an implementation detail rather than pulling in anything new.
The third reason in the README is the compiler itself: utilizing the Rust high-performance compiler and powerful LLVM backend with highly optimized memory access and deeply thought algorithms. That is the least interesting of the three, since it applies to any Rust library, and it is also the least actionable, since it is not a reason to pick this library over another written in the same language.
as_async and to_sync are the API you will actually use
The usage section shows two examples, and the second half of each is the interesting one.
The sync example creates a bounded channel with a capacity for 8 messages, clones the sender, sends from a spawned thread, and receives in the main thread. Then it converts the channel for async use:
// Convert and use channel in async context to communicate between sync and async
tokio::spawn(async move {
// Borrow the channel as an async channel and use it in an async context ( or convert it to async using to_async() )
sender.as_async().send("hello").await?;
anyhow::Ok(())
});The async example mirrors it. It creates a bounded channel with a capacity for 8 messages with kanal::bounded_async(8), sends twice with await, clones the receiver and converts it to a sync receiver, and then uses the original receiver in a tokio task and the converted one in a plain thread:
// Clone receiver and convert it to a sync receiver
let receiver_sync = receiver.clone().to_sync();
tokio::spawn(async move {
let msg = receiver.recv().await?;
println!("I got msg: {}", msg);
anyhow::Ok(())
});
// Spawn a thread and use receiver in sync context
std::thread::spawn(move || {
let msg = receiver_sync.recv()?;
println!("I got msg in sync context: {}", msg);
anyhow::Ok(())
});Two conversion styles appear. as_async borrows the channel for an async context, and the comment mentions the alternative of converting it with to_async(). to_sync converts a receiver you have just cloned, which is why the clone is there.
The result is that one channel serves a thread that blocks and a task that awaits, which is the scenario that makes the standard library awkward. The README lists the flexible API of as_sync and as_async among the reasons to use the library, and the two examples show why it is more than a convenience.
Close broadcasts like Go, and MPMC and SPSC live together
Two features in the list of reasons are worth pulling out, because they change how a channel is used rather than how fast it is.
The first is closing. Similar to Golang, Kanal allows you to close channels using the Close function, enabling you to broadcast a close signal from any channel instance and close the channel for both senders and receivers. That is a real difference from the usual model, where closing is a one way operation performed by the sender and observed by receivers, and where closing a sender does not close its clones. Here, any instance can close, and the close is seen by everyone, which is what makes it usable as a shutdown broadcast across a set of workers.
The second is that the package contains high-performance MPMC, multiple producers and multiple consumers, and SPSC, single producer and single consumer, channels in a single package. Most libraries make you choose a type per use case, and the choice is a type name that appears in every function signature. Kanal gives you one channel type that supports both shapes, which is also what makes the sync and async conversion work across a whole tree of senders and receivers rather than one pair.
For a reader weighing alternatives, the comparison is not simply about speed. The standard library's mpsc is a multi producer, single consumer channel, and the async channel in the ecosystem is a different type with a different error type. Kanal is the library for the case where you want one channel that works in both worlds, with a close that everyone sees, and where the producers and consumers are not fixed in number at compile time.
loom checks the handoff, the parking and the termination
The last item in the reasons list is the one that says most about how the library is built, and it is the least likely to be familiar to people who have not worked on concurrent Rust.
Kanal's concurrency protocol is model-checked with loom. The send and receive handoff, the parking and termination paths are exhaustively verified against the C11 memory model, including data-race detection on the internal signal buffers. And the README gives the command to run the models:
RUSTFLAGS="--cfg loom" cargo test --release --lib --test loom --all-featuresThis is the right tool for the problem. A concurrent channel has a small number of states that are hard to hit by running the tests, and loom enumerates the interleavings systematically instead of hoping a scheduler produces them. The fact that the internal signal buffers are checked for data races specifically is the part that matters, because those buffers are where a missed wakeup or a reordering would turn into a hang or a lost message.
The manifest shows the plumbing. The loom dependency is not a normal dependency at all, it sits in a target specific section for cfg(loom), with loom 0.7 and its futures feature, and the comment explains that it is a model checker for concurrent code active only with the RUSTFLAGS cfg loom. There is a corresponding lint configuration, unexpected_cfgs set to warn with check-cfg for cfg(loom), so the conditional compilation is declared rather than silently accepted.
The test file is referenced in the manifest comment as tests/loom.rs, with a note on how to run the loom test suite, and the repository root carries a pre_push.sh script, which suggests the model check is meant to be part of the pre push routine rather than something a contributor has to remember.
The benchmark harness and what its numbers can tell you
The benchmark section is the part to read carefully, because a channel library's numbers are entirely a function of the workload.
Results are based on how many messages can be passed in each scenario per second. There are four test types. Seq is sequentially writing and reading to a channel in the same thread. SPSC is one receiver and one sender passing messages between them. MPSC is multiple sender threads with only one receiver. MPMC is multiple senders and multiple receivers communicating through the same channel. And two message types, usize tests transferring messages of size hardware pointer, and big tests transferring messages of 8x the size of the hardware pointer.
The table also marks N/A where the test subject is unable to perform the test due to its limitations, with the note that some test subjects do not have an implementation for size 0 channels, MPMC or unbounded channels. That column is informative in itself, since it maps out what each implementation can and cannot do.
The environment is stated precisely, which is what makes the numbers comparable: an AMD Ryzen 9 9950X processor, rustc 1.85.1, go1.24.1 on linux/amd64, and Linux 6.11.0-19-generic on Ubuntu, dated Mar 19, 2025. The benchmark code lives in a separate repository, fereidani/rust-channel-benchmarks, so the harness is inspectable rather than a claim in a README.
The manifest holds the two benchmark targets, sync and async, both with harness = false, which is what criterion requires, and criterion 0.8 is a development dependency alongside crossbeam 0.8 and tokio, so other channels are run in the same harness.
The README also explains a result that looks contradictory at first, why async outperforms sync in some tests. It is about the context switching performance of libraries such as tokio, which, similar to Golang, use context switching within the same thread to switch to the next coroutine when a message is ready on a channel, which is more efficient than communicating between separate threads. And as the channel size increases, synchronous benchmarks may improve, since the sending threads can push data directly to the channel queue without awaiting blocking or suspending signals from receiving threads.
That last observation is the practical lesson. A larger buffer changes the shape of the work, and the number that wins for a size 8 channel is not necessarily the number that wins for a large one.
Editorial conclusion
Adopt kanal if your Rust code has both blocking and async parts that need to talk to each other, since that is the case it handles better than separate sync and async channel types. Do not adopt it in production without pinning to a released version, because the crate version in the repository is 0.2.0-beta1 while the README still tells you to depend on 0.1. Verify first which version you actually depend on, that the loom suite passes with the RUSTFLAGS cfg loom invocation, and that your benchmark numbers come from a harness whose workload matches your own.
Frequently asked questions
How do I add kanal to a Rust project?
The README gives the dependency as kanal = "0.1" in Cargo.toml, while the repository's Cargo.toml declares version 0.2.0-beta1, so check which line you are actually depending on. The crate is published on crates.io with documentation on docs.rs.
How do I use a sync channel from async code in kanal?
Call as_async() on the sender to borrow the channel in an async context, or convert it with to_async(), and then send with await. The reverse direction exists too, with a receiver clone converted by to_sync() and used from a plain thread.
Does kanal support model checking of its concurrency?
Yes. The README states that the concurrency protocol is model-checked with loom, with the send and receive handoff, parking and termination paths exhaustively verified against the C11 memory model, including data race detection on the internal signal buffers. The models are run with RUSTFLAGS="--cfg loom" cargo test --release --lib --test loom --all-features.
How do you close a kanal channel?
Similar to Golang, Kanal allows you to close channels using the Close function, which broadcasts a close signal from any channel instance and closes the channel for both senders and receivers. The package also includes MPMC and SPSC channels in a single package.
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/fereidani-kanal)Star history chart for your README
[](https://hysenlabs.com/projects/fereidani-kanal)Was this useful?