go-disruptor: the LMAX Disruptor pattern in Go
A port of the LMAX Disruptor to the Go language.
At a glance
- What is it?
- go-disruptor is a Go port of the LMAX Disruptor, a ring buffer that hands producers a reserved sequence, lets them write into pre-allocated space and commit the range, so consumers read a constant rate stream with no locks on the hot path and no garbage to collect. The README is refreshingly blunt about when not to use it, and publishes the benchmark tables that justify the complexity.
- Who is it for?
- Adopt go-disruptor if you have a measured hot path where a channel's per operation cost shows up in a profile, and if the team can live with a hand managed ring buffer and the assembly files that come with it. Do not adopt it for ordinary request handling, where the README itself says channels remain the easiest and most desirable choice for the vast majority of use cases.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 24 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the Disruptor pattern is, once you are in Go
The README starts with the relationship to the original. This is a port of the LMAX Disruptor into the Go programming language, and it retains the essence and spirit of the Disruptor while using the same underlying abstractions and concepts, but it does not maintain the names or API.
That last clause is the one to hold on to. This is not a drop-in translation where the LMAX method names carry over, so anybody arriving with the Java API in their head will be reading new documentation, not recognising old names.
The design rationale is stated in one paragraph, and it has two halves. Once initialized and running, one of the preeminent design considerations of the Disruptor is to process messages at a constant rate. It does this using two primary techniques. First, it avoids using locks which contend between CPU cores, thus interfering with and ultimately preventing scaling. Secondly, the Disruptor produces no garbage, by pre-allocating contiguous space on a ring buffer. By avoiding garbage altogether, the need for garbage collection and the stop-the-world application pauses it introduces can be almost entirely avoided.
So the promise is two distinct properties, not one. No locks means no contention between cores, and no allocation means no collector pause. The second is the more interesting one in Go, because the garbage collector is the price of the language's memory safety, and this pattern is a way of paying it up front instead.
The repository is a Go module named github.com/smarty/go-disruptor with the go directive at 1.26, and the last push was on 2026-09-29.
Reserve, write, Commit, and the ring buffer you own
The example in the README is the whole API in one file, and it is worth reading closely because the publishing path is three instructions and the ring buffer is yours.
instance, err := disruptor.New(
disruptor.Options.BufferCapacity(bufferSize),
disruptor.Options.WriterCount(1), // set the value to 2+ if we intend to have a multiple, concurrent writers
disruptor.Options.NewHandlerGroup(consumer1Group1, consumer2Group1), // these consumers run in parallel
disruptor.Options.NewHandlerGroup(consumer1Group2, consumer2Group2)) // these consumers run in parallel AFTER the first group has completedThe configuration is where the semantics live. BufferCapacity sets the ring size, WriterCount says how many producers may publish concurrently, and NewHandlerGroup creates a consumer group. The comment on the second group is the sequencing rule of the pattern: those consumers run in parallel after the first group has completed.
Publishing is then the three step path the README describes. Reserve a slot or a range, write into the ring buffer at the reserved sequence, and commit the range so the data becomes available to the consumers.
// EXAMPLE: Claim a single slot or entry
reservedSequence := instance.Reserve(1)
// now that we have a reserved slot, write to it
ringBuffer[reservedSequence&bufferMask] = 42
// commit the write to make the data available to the configured consumers
instance.Commit(reservedSequence, reservedSequence)Note that the ring buffer is a package level array in the example, and that the sequence is masked with bufferMask to get the index. The buffer size must be a power of 2, with the example using 1024 * 64 and the mask as bufferSize - 1.
On the consumer side, a handler implements Handle with a lower and an upper sequence, and instance.Listen() blocks until the instance is closed and all the enqueued work has been completed.
The benchmark tables, and the machine they came from
The README publishes per operation timings for channels and for the Disruptor, and it states the hardware, which is the part that makes them comparable.
Each benchmark sends an incrementing sequence message from one goroutine to another, and the receiving goroutine asserts that the message received is the expected incrementing sequence value, with any failures causing a panic.
The machine is an Intel Core i7-12700K, on Linux 6.18, with the Go runtime at 1.25.
The channel numbers, with a 16K buffer, are 32.4 ns for single producer and single consumer blocking, 141.8 ns for multi producer with 4 writers blocking, 159.0 ns for single producer and single consumer non blocking, and 531.9 ns for multi producer with 4 writers non blocking.
The Disruptor numbers, with a 64K buffer and a single writer, are 6.5 ns for a reserve of 1 and 0.4 ns for a reserve of 16, 6.8 ns and 0.5 ns for single producer with multiple consumers, and with multiple writers and 4 writers, 32.3 ns and 3.1 ns for a single consumer, and 34.3 ns and 3.1 ns for multiple consumers.
Two things are worth drawing out. The reserve of 16 numbers are lower than the reserve of 1 numbers, because one call that covers sixteen slots amortises the sequence bookkeeping over sixteen messages. And the multiple writer case costs roughly five times the single writer case for a reserve of 1, so the contention that the design removes is not free, it is merely smaller than a channel's.
The README's own framing of the scale is that on modern desktop hardware it can pass many hundreds of millions of messages per second from one goroutine to another, and that a Go channel maintains a mutex around send, receive and len operations, giving it a maximum uncontended throughput of 20-30 million messages per second, more than an order of magnitude slower than the Disruptor, and about 5-7 million messages per second when contended between OS threads.
The build runs the test suite twice on purpose
The Makefile contains a decision that explains a lot about the repository, and it is stated in a comment.
Each target runs twice, with -race and without it, because non-race amd64 builds replace the sync/atomic commit store with an assembly release store that race builds never compile.
test:
go test -timeout=1s -short -race -covermode=atomic ./...
go test -timeout=1s -short -count=1 ./...The reason is visible in the file listing. There are store_release_amd64.go and store_release_amd64.s alongside store_release_atomic.go, and the assembly path is only compiled for amd64 and only outside race builds, because the race detector instruments the atomic path and the two cannot coexist in one binary.
So the same test suite runs twice, once with the instrumented atomic store and once with the fast assembly store, and the second run is the one that exercises the code that production will actually run. That is an unusual and careful arrangement, and it is the sort of thing that only matters in a library whose entire value proposition is nanosecond numbers.
The rest of the Makefile is conventional. There is a long variant, test.long, which runs TestEndToEnd with a 120 second timeout and verbose output, again twice with and without the race detector. The benchmark target runs go test -bench=. -benchmem, compile runs go build ./..., and build is test followed by compile.
The repository layout backs up the picture. There are sequence.go, sequencer.go, sequencer_shared.go and sequence_barrier.go for the sequencing, listener.go and listener_composite.go for the consumer side, config.go and interfaces.go for the setup, and a family of files whose names describe the hardware work: cpu_padding_32bit.go through cpu_padding_256bit.go, spin_hint_amd64.s, spin_hint_arm64.s, spin_hint_asm.go and spin_hint_other.go, and wait_strategy_spin.go.
When in doubt, use channels
The README has a section titled When In Doubt, Use Channels, and it is the most important paragraph in the document for anybody deciding whether to adopt this.
The argument is that despite Go channels being significantly slower than the Disruptor, channels should still be considered the easiest, best, and most desirable choice for the vast majority of all use cases. The Disruptor's target use case is ultra-low latency environments where application response times are measured in nanoseconds and where stable, consistent latency is paramount and latency spikes cannot be tolerated.
That is an unusually clear scope statement, and it should be read as a precondition rather than a caveat. The library is not a general purpose faster channel. It is a specific structure for a specific environment, and the author says plainly that in almost every other case you should use the built in one.
The repository also carries a TODO.md and a FINDINGS.md at the root, plus a CLAUDE.md, a CONTRIBUTING.md and a .golangci.yml for linting. And there is a pre-release note in the README, under the heading Pre-Release Candidate, which records that due to improvements in the Go memory model as of version 1.19, the project was finally updated to utilize sync/atomic, and the section is cut off there.
That note is the closest thing the repository has to a status page, and it tells you the project spent a long time not being able to express its core idea in Go terms, and that the current implementation is the one that finally could.
Editorial conclusion
Adopt go-disruptor if you have a measured hot path where a channel's per operation cost shows up in a profile, and if the team can live with a hand managed ring buffer and the assembly files that come with it. Do not adopt it for ordinary request handling, where the README itself says channels remain the easiest and most desirable choice for the vast majority of use cases. Verify first that your latency spikes are the problem you think they are, that you can keep the buffer size a power of two, and that the assembly accelerated release store behaves correctly on your architecture, since the build runs the suite twice for exactly that reason.
Frequently asked questions
What is go-disruptor and how does it relate to the LMAX Disruptor?
It is a port of the LMAX Disruptor into Go that retains the essence and spirit of the original and uses the same underlying abstractions and concepts, but it does not maintain the names or API. Its design avoids locks, which would contend between CPU cores, and produces no garbage by pre-allocating contiguous space on a ring buffer.
How do I publish a message with go-disruptor?
Reserve a slot or a range with instance.Reserve, write your data into the ring buffer at the reserved sequence, then call instance.Commit with the reserved range so the data becomes available to the consumers. The example in the README shows both a single slot and a range of sixteen.
When should I use go-disruptor instead of a Go channel?
The README says channels remain the easiest, best and most desirable choice for the vast majority of use cases, and that the Disruptor targets ultra-low latency environments where response times are measured in nanoseconds and latency spikes cannot be tolerated.
Why does the go-disruptor build run its tests twice?
The Makefile comment explains that non-race amd64 builds replace the sync/atomic commit store with an assembly release store that race builds never compile, so each target runs once with -race and once without it to cover both paths.
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/smarty-go-disruptor)Was this useful?