# Disruptor-net: a .NET port of the LMAX circular queue for inter-thread messaging

> Disruptor-net is a C# port of the LMAX Disruptor, a ring buffer of pre-allocated events with a configurable sequence of consumers, offered on NuGet as the Disruptor package. It suits low latency .NET services where allocation and blocking are the enemies, and the 7.x line adds inter-process communication and Native AOT while still sitting in release candidate form.

**disruptor-net/Disruptor-net** — Port of LMAX Disruptor to .NET

- Repository: https://github.com/disruptor-net/Disruptor-net
- Stars: 1,333 · Forks: 225
- Language: C#
- License: NOASSERTION
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/disruptor-net-disruptor-net

## A ring buffer of pre-allocated events

The Disruptor is described as a high performance inter-thread message passing framework, and this project is the .NET port of the LMAX Disruptor.

The definition given is the one to hold on to. The Disruptor can be succinctly defined as a circular queue with a configurable sequence of consumers. That is more than a slogan, because it describes both the data structure and the scheduling model. The data structure is a ring buffer of slots. The scheduling model is that you attach handlers to the ring in a defined order, and events flow through that chain.

The key features are listed as zero memory allocation after initial setup, with the events pre-allocated, push-based consumers, optional lock-free operation, and configurable wait strategies.

The allocation point is the one that matters most in a managed runtime. In a typical producer-consumer setup, each message is a heap object that the garbage collector eventually has to trace and reclaim, and that cost lands on a latency-sensitive path. Here the event objects exist from the start, and publishing means claiming a slot and overwriting it, which is why the pre-allocation claim is the feature the whole design is built around.

Lock-free is stated as optional rather than guaranteed, which is the honest framing. A ring buffer can be implemented with interlocked operations instead of a lock, but the wait strategy still has to park a consumer thread that has nothing to do, and parking is where the operating system gets involved.

## Four moving parts: event, handler, ring buffer, wait strategy

The basic usage shows the whole API in four pieces, and it is short enough to read in one go.

First, the event type, which is your own class:

```cs
public class SampleEvent
{
    public int Id { get; set; }
    public double Value { get; set; }
}
```

Second, the consumer, which implements `IEventHandler<SampleEvent>` and receives the event, the sequence number and a flag saying whether this is the last event in the batch:

```cs
public class SampleEventHandler : IEventHandler<SampleEvent>
{
    public void OnEvent(SampleEvent data, long sequence, bool endOfBatch)
    {
        Console.WriteLine($"Event: {data.Id} => {data.Value}");
    }
}
```

Third, the ring buffer and the handler chain, with the size given as a named argument:

```cs
var disruptor = new Disruptor<SampleEvent>(() => new SampleEvent(), ringBufferSize: 1024);

disruptor.HandleEventsWith(new SampleEventHandler());

disruptor.Start();
```

Fourth, publishing, which happens inside a scope:

```cs
using (var scope = disruptor.PublishEvent())
{
    var data = scope.Event();
    data.Id = 42;
    data.Value = 1.1;
}
```

Two details in that code are worth pausing on. The `endOfBatch` flag exists because the Disruptor delivers in batches when a producer gets ahead, so a consumer can distinguish a single event from a run of them and amortise its own per-event overhead. And the publish scope exists so that the sequence only advances when the block completes, which keeps a partially written event from ever becoming visible to a consumer.

The `ringBufferSize` argument is a power of two, 1024 in the example, and it is a capacity decision you make up front, not a limit you hit later.

## Wait strategies are the part you choose per workload

The wiki links in the feature list point to Event Handlers, Wait Strategies and, for version 7, IpcDisruptor, and the wait strategy deserves its own look because it is where the latency and CPU trade-off actually lives.

A consumer that has drained the ring buffer has to wait for the next event. There are fundamentally different ways to do that. A consumer can spin on a volatile read of the sequence counter, which gives the lowest possible wake-up latency and burns a core. A consumer can block on a semaphore, which uses no CPU while idle and pays the cost of a context switch and a scheduler wake-up on every event. A consumer can yield, or sleep for a fixed interval, which sits in between and adds a fixed delay.

Because the strategy is configurable rather than baked in, the same ring buffer can be tuned for different parts of a system. A busy trading path might spin, and a batch reporting path might block. The Disruptor design came out of a trading system, and that origin is visible in the shape of the API, since the strategy is a first class choice rather than an implementation detail you can only reach by editing the library.

The handler chaining works the same way. `HandleEventsWith` attaches a handler to the chain, and the wiki has a separate page on Event Handlers describing the available arrangements, which include running handlers in sequence, in parallel, or gating one on another. That gating is what lets one consumer decide whether an event should reach the next one, which is a different capability from a plain work queue.

## Version 7 adds inter-process messaging and Native AOT

Since version 7, the Disruptor also supports two things that change what it can be used for.

The first is inter-process communication using IpcDisruptor, which is documented on its own wiki page. A ring buffer is normally a shared memory structure within one process, and moving it across a process boundary changes the problem: the sequence counters have to be visible to both sides, and the memory has to be shared rather than merely shared within a process. The separate IpcDisruptor type existing at all suggests the implementation treats that as a different beast from the in-process ring buffer rather than a transparent mode.

The second is Native AOT. This is a real constraint on what you can build with a library, since Native AOT forbids runtime code generation, so a library that works under it has to avoid reflection-based tricks and dynamic dispatch that needs JIT. For a messaging framework whose whole purpose is to be fast, being usable in a trimmed, ahead-of-time compiled deployment matters, and it constrains the implementation.

Neither feature is in a stable release yet. The release list shows 7.0.0-rc1 from 2025-09-07 and 7.0.0-rc2 from 2026-04-11, while the latest stable is 6.0.1 from 2025-06-15. A release candidate is the right thing to test against, and the wrong thing to build a long-lived service on without pinning the version carefully.

The last push was on 2026-04-11, and the repository is not archived.

## Runtimes, packaging and where the licence text actually is

The supported runtimes are.NET 6.0 and later, plus.NET Standard 2.1. That second entry is the more interesting of the two, because.NET Standard 2.1 is what a library targeting a wider set of consumers would declare..

The package is published to NuGet under the name `Disruptor`, not under the repository name, and the README links the package and the change list for each major line separately, with the tag filtered release pages for version 6 and version 7. NuGet version numbers carry the major line in the first component, so a 6.0.1 package and a 7.0.0-rc2 package are distinguishable at a glance in a project file.

The repository is small in structural terms. It holds a `src/` directory, a `build/` directory, an `.editorconfig` file, a `LICENSE` file and the usual GitHub configuration. There is no `test/` directory at the top level, and the documentation points at the wiki for a more detailed introduction rather than keeping the long form guide in the repository.

The licence needs one careful sentence. The README states the licence as Apache License, Version 2.0, with the copyright attributed to Olivier Deheurles and the standard Apache notice text, while the repository metadata for the licence field classifies it as a custom licence that GitHub cannot classify, pointing at the LICENSE file. Both statements can describe the same Apache notice text, since the standard Apache file is a long document with a custom appendix structure that automated classifiers often fail to match. If you intend to depend on this package commercially, read the LICENSE file directly rather than relying on either label.

## Where the Disruptor is the wrong tool

The Disruptor is a specialist structure with a real cost, and the honest case against it is worth making.

The first is complexity. A plain `BlockingCollection` or `Channel<T>` is a few lines, needs no sequence numbers, no wait strategies and no handler graph, and is understood by everyone on the team. The Disruptor requires the team to reason about sequence numbers, buffer sizing as a power of two, and the difference between a blocking and a spinning consumer. That cost is paid in every future debugging session.

The second is that the pre-allocation model requires knowing your event size up front. The ring buffer holds a fixed number of pre-allocated event objects, so a large payload multiplied by a large ring buffer is a lot of memory committed at startup, and the event type must be reusable rather than a one-off object per message. If your messages vary wildly in size or are not fixed-shape, a different structure fits better.

The third is that a ring buffer is a single-machine structure. The 7.x IpcDisruptor work extends it across processes, but a distributed system with several machines needs something else, and the Disruptor is not a distributed queue.

The alternative approach is a channel or a concurrent queue, and the difference in approach is exactly the wait strategy: a channel parks the consumer thread, which costs a wake-up but no CPU, while the Disruptor offers a spinning strategy that trades a core for the lowest latency. The Disruptor wins when latency in microseconds matters and a core is available to burn. It loses when throughput per core across many threads matters more, when the message rate is low enough that a wake-up is irrelevant, or when the team would rather spend its complexity budget elsewhere.

The right way to answer the question is to measure. The LMAX origin story is about a specific latency problem, and the same structure applied to a workload that does not have that problem is just a more complicated queue.

## Conclusion

Disruptor-net fits a .NET service with a measured latency problem where a queue, a channel or a blocking collection is showing up in a profile, because the ring buffer avoids allocation on the hot path and lets you choose a wait strategy rather than inherit one. Skip it if your workload is not actually contended, since the complexity buys nothing when a producer and a consumer never overlap, and be careful with the 7.x line, where inter-process communication and Native AOT support are only in release candidate form. Before you upgrade from 6.0.1, read the release notes for the 7 tag, since 7.0.0-rc1 and 7.0.0-rc2 both carry behavioural changes, and note that the licence in the repository is stated as Apache License 2.0 in the README while the repository metadata classifies the LICENSE file as a custom licence GitHub cannot classify.

## FAQ

### What is Disruptor-net and what is the Disruptor?

It is the .NET port of the LMAX Disruptor, a high performance inter-thread message passing framework defined as a circular queue with a configurable sequence of consumers.

### What runtimes does the Disruptor package support?

The supported runtimes are .NET 6.0 and later, and .NET Standard 2.1.

### How do I publish an event with Disruptor-net?

Call `disruptor.PublishEvent()` to get a scope, fill in the event returned by `scope.Event()` and let the scope dispose, which advances the sequence and makes the event visible to consumers.

### What is new in version 7 of Disruptor-net?

Since version 7 the Disruptor supports inter-process communication through IpcDisruptor and supports Native AOT. Version 7 is still in release candidate form, with 7.0.0-rc2 as the latest, while 6.0.1 is the latest stable.

### What licence does Disruptor-net use?

The README states Apache License, Version 2.0, with the copyright attributed to Olivier Deheurles, and points to the LICENSE file in the repository.

## Sources

- [disruptor-net/Disruptor-net on GitHub](https://github.com/disruptor-net/Disruptor-net)
- [Issues](https://github.com/disruptor-net/Disruptor-net/issues)
- [README](https://github.com/disruptor-net/Disruptor-net/blob/master/README.md)
- [Releases](https://github.com/disruptor-net/Disruptor-net/releases)

---

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