ringbuf: a lock-free SPSC ring buffer with direct access to inner data
Lock-free SPSC FIFO ring buffer with direct access to inner data
At a glance
- What is it?
- agerasev/ringbuf is a Rust single-producer single-consumer FIFO buffer whose endpoints expose the underlying storage directly, with static and overwriting variants. It suits fixed-capacity producer and consumer pipelines that cannot afford to block.
- Who is it for?
- Adopt ringbuf when you have exactly one producer and one consumer, a fixed capacity you control, and a hard requirement that no operation may wait, and prefer LocalRb for single-threaded work or StaticRb when the allocator is off the table. Do not adopt it for multi-producer pipelines, for concurrent overwrite mode, or as a general purpose channel.
- 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 14 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
One producer, one consumer, and no waiting
ringbuf solves a narrow problem: handing items from exactly one producer to exactly one consumer through a fixed-capacity FIFO queue without taking a lock. The project describes the operations as lock-free, meaning they succeed or fail immediately rather than blocking or waiting, which is why the producer side exposes `try_push` and the consumer side exposes `try_pop` instead of parking the calling thread. That definition rules out the fan-in and fan-out topologies general purpose queues tend to support. One producer thread and one consumer thread is the entire model. If a pipeline needs two producers writing into the same queue, or a consumer reading on behalf of several readers, this crate is the wrong shape. Items do not have to be `Copy`, and the buffer also implements `Read` and `Write`, so it can sit behind the standard trait surface where that matters.
A rejected push hands the item back to the caller
Splitting the buffer into a `Producer` and a `Consumer` is the entry point for daily use. `HeapRb` is recommended for most cases because it stores contents in dynamic memory, and `split()` returns that pair of endpoints. The project's simple example gives a buffer of capacity two, fills it, and shows the third push rejected with the value returned to the caller:
use ringbuf::{traits::*, HeapRb};
let rb = HeapRb::<i32>::new(2);
let (mut prod, mut cons) = rb.split();
prod.try_push(0).unwrap();
prod.try_push(1).unwrap();
assert_eq!(prod.try_push(2), Err(2));
assert_eq!(cons.try_pop(), Some(0));
prod.try_push(2).unwrap();
assert_eq!(cons.try_pop(), Some(1));
assert_eq!(cons.try_pop(), Some(2));
assert_eq!(cons.try_pop(), None);Because `try_push` returns `Err` carrying the item instead of dropping it, a value the queue refused is never lost. The example then pops the first item, pushes `2` into the freed slot, and reads the remaining values in FIFO order until `try_pop` returns `None`. Capacity is bounded between `1` and `usize::MAX / 2`, and the constructors panic when the requested capacity is zero or above that limit. The same ceiling applies to zero-sized items, so a buffer meant for marker types still consumes slot budget.
StaticRb keeps the queue out of the allocator
For embedded and kernel-adjacent builds the crate can shed both the standard library and the allocator. `StaticRb` stores contents in statically allocated memory, and the no-heap example constructs one with a const size, splits it with `split_ref()`, and shows that a second push into a capacity of one is rejected with `Err(321)` while the first value comes out intact:
use ringbuf::{traits::*, StaticRb};
const RB_SIZE: usize = 1;
let mut rb = StaticRb::<i32, RB_SIZE>::default();
let (mut prod, mut cons) = rb.split_ref();
assert_eq!(prod.try_push(123), Ok(()));
assert_eq!(prod.try_push(321), Err(321));
assert_eq!(cons.try_pop(), Some(123));
assert_eq!(cons.try_pop(), None);The Cargo manifest lists `std` as a default feature and keeps `alloc`, `portable-atomic` and `portable-atomic-util` as separate opt-ins, and the categories field places the crate under `no-std`. The optional `portable-atomic` dependency exists so the same code can run on smaller systems that lack compare-and-swap instructions. The workspace wires the crate to itself with default features turned off:
ringbuf = { path = ".", version = "0.5.3", default-features = false }The project gives no install command of its own. It points readers to the crates.io listing and to the docs.rs documentation, so installation arrives through ordinary Cargo dependency resolution rather than a scripted step.
push_overwrite evicts the oldest item but demands exclusive access
Overwrite mode changes the contract. When the buffer is full, a new insertion replaces the oldest element instead of failing. The overwrite example creates a two slot `HeapRb` and shows the third `push_overwrite` returning the evicted value:
use ringbuf::{traits::*, HeapRb};
let mut rb = HeapRb::<i32>::new(2);
assert_eq!(rb.push_overwrite(0), None);
assert_eq!(rb.push_overwrite(1), None);
assert_eq!(rb.push_overwrite(2), Some(0));
assert_eq!(rb.try_pop(), Some(1));
assert_eq!(rb.try_pop(), Some(2));
assert_eq!(rb.try_pop(), None);The catch is stated plainly: `push_overwrite` requires exclusive access to the ring buffer, so performing it concurrently means guarding the buffer with a mutex or another lock. That is a genuine limit for a crate whose central promise is lock-free access, because the overwrite path reintroduces blocking and does not compose with the shared producer and consumer split. The approach fits a telemetry or logging buffer where the newest sample matters more than the oldest one. The example files include `examples/overwrite.rs` and `examples/simple.rs`, both gated behind the `alloc` feature in the manifest.
The frozen endpoint API is deprecated and its timing changed
Upgraders need to know about the compatibility layer. The project notes that deferred publication in frozen endpoints and in `PopIter` could lead to double drops if they were forgotten with `core::mem::forget`, and that publication no longer depends on running their destructors. `Frozen`, `FrozenProd`, `FrozenCons` and `freeze()` are deprecated compatibility APIs whose removal is reserved for a future breaking release. Existing calls remain available, but the semantics underneath them moved. The stated migration path is to use `CachingProd` and `CachingCons` directly and remove calls to `freeze`, `commit`, `fetch` and `sync`, because operations now publish their changes immediately and fetch the opposite endpoint's progress as needed. Two behavior changes affect correctness rather than compilation. `FrozenProd::discard` is now a no-op, so items that may need discarding must be staged outside the ring buffer and inserted only when they are ready to publish. `PopIter` releases each slot as soon as it yields the item, its `commit` method is deprecated and does nothing, and it still yields only the items that were present when it was created. Code relying on delayed visibility or on a rollback path has to be rewritten.
Cache synchronization is what sharing between cores costs
The performance notes are blunt about where the cost sits. `SharedRb` has to synchronize the CPU cache between cores, and that synchronization carries overhead. The suggested way to limit it is to use the methods that operate on many items at once, `push_slice` and `push_iter` on the producer side and `pop_slice` on the consumer side, so that one synchronization covers many items instead of one per item. Caching endpoints also avoid repeatedly fetching the opposite endpoint's index when progress is possible, and completed operations publish their index updates immediately, including each step of `pop_iter`. Destructive operations scale differently by item type. `skip` and `clear` take constant time for items without destructors, such as `u8`. Items that need destruction are dropped individually, with each slot kept occupied until its destructor finishes, so a queue of `String` or other owning types pays a per-item drop cost where a byte queue does not. For single-threaded usage the same notes recommend `LocalRb`, a type intended only for single-threaded use, since it is slightly faster than `SharedRb` with no cache synchronization at all.
Async and blocking variants live in separate crates
The core crate gives you non-blocking `try` operations and nothing else. If a consumer should wait for items rather than poll, or if the queue should sit inside an async task, the answer is a companion crate rather than a feature of this one. The project lists two derived crates, `ringbuf-blocking` and `async-ringbuf`, and the Cargo workspace declares `async` and `blocking` as its members. The difference in approach is the waiting semantics: the core `ringbuf` returns immediately with a success or a failure, the blocking variant waits on the operation, and the async variant exposes the same buffer through async interfaces. Choosing the core crate alone and then looking for a blocking or async method inside it will not work, so the decision is which of the three crates to depend on. The manifest also carries a `bench` feature and a `test_local` feature, the latter paired with the `test_local` flag used for single-threaded testing paths such as `LocalRb`. For a pipeline that already runs on a work-stealing runtime, taking the async crate keeps the producer and consumer endpoints in the same shape as the plain version while letting the consumer await the next item.
A release history that trails the manifest version
Version numbers deserve a check before you pin anything. The manifest declares `version = "0.5.3"` for the `ringbuf` package, and the workspace depends on that same version with default features disabled. The release history recorded for the project lists `v0.4.0` dated 2024-04-25, ahead of `v0.3.2` from 2023-01-17 and `v0.3.0` from 2022-10-08. The tags in that list therefore do not line up with the version the workspace is currently building, so confirm on crates.io which release your dependency actually resolves to rather than assuming the manifest version is what you get. Licensing is permissive and dual. The manifest records `MIT OR Apache-2.0`, and the project states that the crate is available under either the Apache License, Version 2.0 or the MIT license at your option, with contributions dual licensed under the same terms unless a contributor states otherwise. A team whose policy favours MIT has a straightforward path; a team with an Apache-2.0 review process still has that route available. That is a statement of what the license text permits, not advice on your obligations. On maintenance, the repository is not archived and the last push was on 2026-10-09.
Editorial conclusion
Adopt ringbuf when you have exactly one producer and one consumer, a fixed capacity you control, and a hard requirement that no operation may wait, and prefer LocalRb for single-threaded work or StaticRb when the allocator is off the table. Do not adopt it for multi-producer pipelines, for concurrent overwrite mode, or as a general purpose channel. Before pinning a version, check which release crates.io actually serves for the ringbuf package, since the workspace manifest reads 0.5.3 while the newest release entry recorded for the project is v0.4.0 from 2024-04-25.
Frequently asked questions
Can ringbuf be used with more than one producer thread?
No. The crate is a single-producer single-consumer structure, and the documented usage pattern is one `Producer` and one `Consumer` split from the buffer. Nothing in the project describes multi-producer support, so a fan-in pipeline needs a different structure.
What happens when try_push is called on a full ringbuf?
The call returns immediately with an error carrying the item, for example `Err(2)`, rather than blocking or dropping the value. The capacity must sit between `1` and `usize::MAX / 2`, and the constructors panic if it is zero or larger than that.
Can ringbuf be used without the standard library or an allocator?
Yes. `StaticRb` stores contents in statically allocated memory, the crate is categorized under `no-std`, and the `std` and `alloc` features are separate opt-ins. The optional `portable-atomic` dependency also allows use on systems without compare-and-swap operations.
How do I make a ring buffer that overwrites the oldest element?
Use `push_overwrite`, which replaces the oldest element when the buffer is full and returns the evicted value. It requires exclusive access to the buffer, so concurrent use means guarding it with a mutex or another lock.
What is the difference between ringbuf, ringbuf-blocking and async-ringbuf?
The core `ringbuf` crate offers lock-free operations that succeed or fail immediately. `ringbuf-blocking` and `async-ringbuf` are separate derived crates that provide the blocking and asynchronous versions of the same buffer.
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/agerasev-ringbuf)Was this useful?