Library / SDK
nicholassm/disruptor-rs avatar
nicholassm/disruptor-rs

disruptor-rs: a ring buffer for Rust threads that trades CPU for latency

Low latency inter-thread communication library in Rust inspired by the LMAX Disruptor.

905 stars52 forksRustMIT

At a glance

What is it?
disruptor is an MIT licensed Rust library for low latency inter-thread communication, inspired by the LMAX Disruptor, where producers write into a ring buffer and processors consume sequences from it. It suits engineers building event pipelines in Rust who are willing to give up CPU to shave latency and raise throughput compared with Crossbeam or std::sync::mpsc channels.
Who is it for?
disruptor fits a Rust engineer building a fixed event pipeline, a market data handler or a log processor, where the workload is predictable enough to size a ring buffer and where the latency budget is tight enough that a channel's allocator and lock overhead matter. It is the wrong choice for irregular work where you cannot bound the buffer, and the wrong choice if you want the flexibility of std::sync::mpsc without thinking about sequence numbers and busy waiting.
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 76 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 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Adding the crate and publishing the first event

The dependency itself is a single line in Cargo.toml, and the README asks for a specific version:

toml
disruptor = "4.4.0"

The full API documentation is on docs.rs/disruptor, and the README points there for details on how to use the library.

The smallest useful program follows the pattern of the README's first example. A factory closure creates the events that fill the ring buffer slots, a processor closure takes the event, the sequence and the end-of-batch flag, and the producer is built with build_single_producer(size, factory, BusySpin), chained through handle_events_with(processor) and finished with build(). The producer then publishes events one at a time with publish, and in a batch with batch_publish, where the iterator handed to the closure is guaranteed to yield as many events as the count argument.

One behaviour in that example is worth understanding before you write your own. When the Producer instance goes out of scope, and once the processor has handled all the events, the Disruptor is dropped as well. That is the shutdown path, and it is worth knowing that the producer handle's lifetime is what ends the run rather than an explicit stop call.

A ring buffer where latency is bought with CPU

disruptor is a low latency, inter-thread communication library written in Rust, heavily inspired by the Disruptor library from LMAX.

The README states the trade-off in one sentence: use it when you want to trade CPU resources for lower latency and higher throughput compared to channels such as Crossbeam or std::sync::mpsc. That framing is the whole design. A general purpose channel is built to be flexible, so it pays for that flexibility in allocation, in locking or in a wakeup. A disruptor is built for a fixed shape of workload, and it spends CPU instead: a processor waiting for work spins rather than sleeps, so that the moment an event is published the consumer picks it up without a scheduling delay.

The data flow itself is the classic disruptor shape. Events live in a preallocated ring buffer, and a factory closure creates the event objects that fill the slots. A producer takes a sequence, writes into the slot, and publishes. A processor follows a sequence cursor, waits for the next available sequence, and processes events in order. Because the buffer is preallocated, the steady state does not allocate, and because consumers track a sequence rather than pulling from a queue, there is no contention on a lock for each message.

The repository layout is small and tells you where to look. There is src/, benches/, a verification/ directory, a Cargo.toml, a rust-toolchain file and a LICENSE. The Cargo.toml names the package disruptor, at version 4.4.0, edition 2021, categorised under data-structures and concurrency, and with keywords covering disruptor, ringbuffer, message and mpmc. The runtime dependencies are few: crossbeam-utils 0.8, core_affinity 0.8.1 for core pinning, and thiserror for error types. Development dependencies add criterion 0.5 for benchmarking and crossbeam 0.8 for comparison.

The audience is Rust engineers who already know what a ring buffer is and want a specific set of semantics rather than a general channel. The README links to docs.rs/disruptor for the full API documentation.

Two ways to consume: managed processors or your own poller

The library offers two consumption models, and choosing between them is the first decision a user makes.

The first is to supply a closure to the Disruptor and let it manage the processing threads. The second is the EventPoller API, where you poll for events and manage your own threads. The README says both have comparable performance, so use whichever fits the use case, and points at the benchmarks.

That framing matters because it means the library is not pushing you toward its own threading model. The managed processor path is a convenience: you give it a closure, it spawns and runs the thread, and the Drop behaviour is described in the example, where the Producer instance going out of scope means the disruptor is dropped once the processor has handled all events. The poller path hands the sequence cursor back to you, and the example shows the shape, with a loop matching on poller.poll(), and on Ok the events guard implements ExactSizeIterator so a batch can be processed with a for loop.

The batching story is the other half of the design, and it is worth being explicit about. The README states that batch publication should be used whenever possible for best latency and throughput. The difference between publishing one event at a time and publishing a batch is the difference between a consumer being woken once per event and being woken once per batch, and on a busy pipeline that difference is where a large part of the throughput comes from.

The publisher API reflects this. The producer handle has a publish method taking a closure that fills a single event, and a batch_publish method taking a count and a closure over an iterator. The example comments are specific about the guarantee: the iterator is guaranteed to yield 5 events, because the count argument is 5. That guarantee is what makes batch processing possible, since the consumer can then hand a known number of events to a consumer that does not have to check for a boundary.

The end_of_batch flag in the processor closure is the counterpart on the consuming side. A processor receives the event, the sequence and a boolean telling it whether this is the end of a batch, so a consumer can flush or commit once per batch rather than once per event.

Pinning cores and wiring processors into a dependency graph

The library supports pinning threads on cores to avoid latency induced by context switching, and the README's advanced example shows what that looks like when several processors and producers are combined.

The builder is where the graph is expressed. The producer is built with build_multi_producer(64, factory, BusySpin), then processors are attached with handle_events_with. The and_then method creates a dependency: the comment in the example says h2 handles events concurrently with h1, and the h3 handler handles events after h1 and h2, which is a fan-in where a downstream processor waits for both upstream ones. Two producers are then created, with the second obtained by cloning the first, and each publishes from its own thread inside a thread::scope.

The core pinning is a method on the chain rather than a separate call, with pin_at_core(1) before handle_events_with(h1), pin_at_core(2) before h2 and pin_at_core(3) before h3. Combined with the core_affinity dependency, that is a direct way to remove scheduler migration from the picture on a machine where the working set fits in a small number of cores.

The wait strategy is the other visible choice, and BusySpin appears in every example. The build functions take a wait strategy as their third argument, and BusySpin is the one the README uses throughout, which is consistent with the stated trade-off of CPU for latency. A spinning consumer burns a core while idle, and on a machine with more consumers than spare cores that is a poor trade. The library supports other strategies, since the parameter is a strategy, but the documentation examples do not discuss the alternatives, so that is a place to read the API docs rather than the README.

The producer and multi producer distinction is the other axis. build_single_producer and build_multi_producer differ in whether the ring buffer's sequence claim has to be contended between producers, and the multi producer path is the one used in the example that spawns two publishing threads.

Processor state, and the option to own your threads instead

There is a specific problem the library solves for processors that need local state, and the answer is a separate closure for initialising that state.

If you need to store state in the processor thread which is neither Send nor Sync, the README's example uses an Rc<RefCell<i32>>, and you create a closure for initialising that state and pass it along with the processing closure when you build the disruptor. The disruptor then passes a mutable reference to your state on each event. The processor closure's signature changes to take that state as its first argument, and the builder method becomes handle_events_and_state_with(processor, initial_state).

This is the kind of feature that decides whether a disruptor can be used at all in a given program. A processor that owns a non-Send type, such as something holding a thread-local cache or a single-threaded handle, cannot be moved between threads, and so cannot live in a normal Arc-shared structure. The disruptor keeps the state inside the processor thread and hands out a mutable borrow per event, which is exactly what a single-threaded owner needs and what a shared-mutex design would have to fake.

The alternative to state is the EventPoller API, which the README presents as an alternative to storing state in the processor. In the poller example, a builder is created, new_event_poller is called on it to get a poller and a builder back, and the builder is then used to build the producer. The poller owns the cursor, and the loop polls for events, with the guard implementing ExactSizeIterator so the events can be batch processed.

One detail in the example is worth flagging because it is the kind of thing that bites: the comment about the guard going out of scope, and the match arm structure, indicate that the events are guarded by a scope and released when the guard drops. A poller that holds a cursor cannot advance until that guard is released, so a consumer that keeps the guard alive across an await point or a blocking call will stall the pipeline. The README's comment is cut off in the visible text, but the structure of the code is the point.

Between the two mechanisms, the library covers both the push model with per-thread state and the pull model with a caller-owned cursor.

What the repository does not say, and what the benchmarks cover

Several things a reader would want are simply not in the README, and it is better to name them than to guess.

The README's table of contents lists sections called Features, Patterns, Design Choices, Correctness, Performance, Related Work, Used By, Contributions and Roadmap, and the visible text covers Getting Started and the processing patterns. The performance section is where the benchmarks are described, and the repository backs it with a benches/ directory. The Cargo.toml declares four criterion benchmarks, spsc, mpsc, poller and counters, each with harness set to false, which is the standard way of running criterion as a standalone benchmark target. spsc and mpsc correspond to the single producer and multi producer paths, poller to the polling path, and counters is a separate measurement.

The verification/ directory is the other one worth knowing about, and the README's table of contents includes a Correctness section. A directory named verification in a concurrency library usually means model checking or a brute force comparison against a simpler reference implementation, and that is exactly the thing you would want before trusting a ring buffer with a spin wait. The README text visible here does not describe what the verification contains, so it is worth opening rather than assuming.

The Dependencies section of the README's table of contents is not in the visible text, but the Cargo.toml tells the story: crossbeam-utils for the atomic utilities, core_affinity for pinning, thiserror for errors. The dev-dependency on crossbeam is there so the benchmarks can compare against it, which is a good sign, since a library that claims an advantage over a competitor usually shows the comparison.

On maintenance, the repository is not archived and the last push was on 2026-08-08, on the main branch. The releases are v4.4.0 from 2026-08-08, v4.3.0 from 2026-05-16 and v4.2.0 from 2026-05-10, so the project is on a 4.x line and has shipped three releases since May. The version is a breaking-change carrier in the 4.x range, and the Cargo.toml in the repository is at 4.4.0, so the dependency version in your manifest and the crate version move together.

The licence is MIT, declared in the Cargo.toml and held in a LICENSE file at the root of the tree, with no obligations beyond attribution for a library used inside a larger work.

Editorial conclusion

disruptor fits a Rust engineer building a fixed event pipeline, a market data handler or a log processor, where the workload is predictable enough to size a ring buffer and where the latency budget is tight enough that a channel's allocator and lock overhead matter. It is the wrong choice for irregular work where you cannot bound the buffer, and the wrong choice if you want the flexibility of std::sync::mpsc without thinking about sequence numbers and busy waiting. Before you adopt it, read the Design Choices section in the README, measure with the criterion benchmarks in the benches/ directory against your own payload, and check the correctness verification in the verification/ directory before trusting a result.

Frequently asked questions

How do I add the disruptor crate to a Rust project?

Add `disruptor = "4.4.0"` to the `Cargo.toml` file. The crate is published on crates.io as disruptor, and the full API documentation is on docs.rs/disruptor.

What is the difference between the managed processor and the EventPoller API?

With a closure you let the Disruptor manage the processing threads, while the EventPoller API lets you poll for events and manage your own threads. The README says both have comparable performance, so the choice depends on the use case.

When should I use batch publication in disruptor-rs?

The README states that batch publication should be used whenever possible for best latency and throughput. The batch_publish method takes a count and a closure over an iterator that is guaranteed to yield that many events, and processors receive an end_of_batch flag so they can flush once per batch.

How do I keep processor state that is not Send or Sync?

The README says to create a closure for initialising that state and pass it with the processing closure when you build the disruptor, using handle_events_and_state_with. The disruptor then passes a mutable reference to your state on each event, so an Rc<RefCell<i32>> can live in the processor thread.

Does disruptor-rs let me pin threads to specific cores?

Yes. The README says the library supports pinning threads on cores to avoid latency induced by context switching, with pin_at_core called on the builder chain before each handle_events_with. The runtime dependency core_affinity 0.8.1 is what provides that.

Official sources

  1. Issues
  2. License: MIT
  3. nicholassm/disruptor-rs on GitHub
  4. README
  5. Releases
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/nicholassm-disruptor-rs.svg)](https://hysenlabs.com/projects/nicholassm-disruptor-rs)

Was this useful?