Open-source project
bogdanovich/siberite avatar
bogdanovich/siberite

Siberite: a LevelDB-backed message queue that speaks the memcache protocol

Siberite is a simple, lightweight, leveldb backed message queue written in Go.

583 stars24 forksGoNOASSERTION

At a glance

What is it?
Siberite is a Go rewrite of Twitter's Kestrel and wavii's Darner that keeps every message on disk rather than in memory, so a queue can be far larger than RAM, and it adds durable cursors and fanout queues on top of the memcache text protocol.
Who is it for?
Adopt Siberite if you need a queue that is larger than memory, that several consumers must read independently, or that has to survive a restart without a broker cluster to run. Do not adopt it if you need blocking reads, message acknowledgement, or a broker that other teams already know how to operate.
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 153 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

The design bet: messages live on disk, not in RAM

Siberite is a simple message queue server with an unusual set of priorities, and they are stated in the opening paragraph.

The first contrast is with in-memory servers like Redis. Siberite is designed to handle queues much larger than what can fit in RAM. The second contrast is with enterprise-level servers such as RabbitMQ, where the point is that Siberite stores all messages out of process, using goleveldb for persistent storage. The result is described as a durable queue server that uses minimal in-resident memory, regardless of the queue size.

That is the whole design in one sentence, and it is a real trade rather than a free win. Disk is slower than memory, so a queue that spills to LevelDB trades throughput for capacity. What you get in return is that the queue size is a function of your disk rather than your heap, that a restart does not lose messages, and that you do not need to think about eviction policies or memory tuning.

The lineage explains the design. Siberite is based on Robey Pointer's Kestrel, a simple distributed message queue, and it is a Go rewrite of twitter/kestrel and wavii/darner. The README says that compared to Kestrel and Darner, Siberite is easier to build, maintain and distribute, consumes significantly less memory than Kestrel, and offers the ability to consume a queue multiple times using durable cursors.

The primary language is Go, the go.mod declares go 1.17, and the dependencies are few: goleveldb for storage, gomemcache for the client side, randbo for randomisation, and testify for the tests.

The memcache protocol, and how multiple servers behave

Siberite follows the same protocol as Kestrel, which is the memcache TCP text protocol. That is a significant practical decision, because it means existing memcache clients speak to it without modification, and the README links a list of compatible clients.

The semantics of the distributed case are worth understanding. Siberite follows the No talking! Shhh! approach to distributed queues. A single Siberite server maintains a set of queues identified by name. Each queue operates as a strictly-ordered FIFO, while querying from multiple Siberite servers results in a loosely-ordered distributed queue.

That is a candid description of what you get. Within one server, ordering is exact. Across servers, it is loose. So a consumer pool reading from several Siberite instances sees messages in an order that is not globally defined, and a workload that depends on strict global ordering is not a fit.

The protocol also inherits a second property from Kestrel, the two-phase reliable fetch. If a client disconnects before confirming that it has processed a message, the message is handed off to the next client. The mechanism is visible in the command set, where an open, a close and an abort variant exist for each queue, and a transaction stays open until it is closed.

The telnet session in the README shows the whole surface. A set stores a message with its expiry, a get retrieves and by default expires it, and a get with a suffix such as open, close, peek or abort changes the transaction behaviour. A stats command returns server counters including uptime, time, version, connection counts, command counts and queue item counts, and flush, delete, flush_all and quit round out the set.

Durable cursors, the feature Kestrel does not have

The first of the two headline features is durable cursors for multiple reads, and it is the reason to use Siberite rather than a plain memcache protocol server.

With the normal get queue syntax, the returned message is expired and deleted from the queue. With the cursor syntax get queue.cursor_name, a durable cursor is initialised. It advances with every read without deleting messages from the source queue, and there is no limit on the number of cursors per queue.

So a queue can be read independently by several consumers, each with its own position, and no consumer's read destroys another consumer's data. That is the pattern for fanning a work queue out to multiple processing pipelines, or for keeping a replayable history alongside a consuming pipeline.

The interaction with the standard syntax is subtle and worth internalising. If you resume reading from the queue using the standard syntax, Siberite will continue deleting already-served messages from the queue head. Any existing cursor that points to an expired message will restart reading from the current queue head on the next read.

Durable cursors also support two-phase reliable reads. Failed reads for each cursor are stored in the cursor's persistent queue and served to other cursor readers.

The second feature is fanout queues, which allow inserting a message into multiple queues simultaneously with the syntax set queue+another_queue+third_queue. Combined with cursors, that gives you the two primitives most queue users actually need, publish to many and read many times, without a broker that has its own concept of exchanges and bindings.

Building and running it from source

The build instructions assume a Go toolchain with a correct GOPATH, and the steps are the pre-modules era ones.

bash
go get github.com/bogdanovich/siberite
cd $GOPATH/src/github.com/bogdanovich/siberite
go build siberite.go
mkdir ./data
./siberite -listen localhost:22133 -data ./data

The flags are visible in that last line. The listen address and port, and the data directory, which the server creates for the LevelDB store. The log output confirms the three things worth checking on a first run, that it is listening, that it is initializing, and which data directory it picked up.

The data directory has to exist before the server starts, hence the mkdir in the sequence. That is a small sign of the project's age, and it connects to the only build step described here: go get followed by go build of the single siberite.go file, with no vendoring step despite a vendor/ directory in the tree.

If you do not want to build, the README points at darwin-x86_64 or linux-x86_64 builds on the releases page, which is a narrow platform matrix, another symptom of the same age.

What it will not do, stated in one line

The Not supported section of the README contains exactly one entry, and it is worth quoting in full because it is the kind of thing that is easy to discover the hard way.

Waiting a given time limit for a new item to arrive, using the protocol's timeout syntax, is allowed by the protocol but does nothing.

That is a genuine feature gap rather than a rounding error. The memcache protocol has a long-poll style get with a timeout, and it is the mechanism a consumer uses to wait for work instead of polling in a loop. In Siberite that call parses and returns without blocking. A consumer that expects it to wait will busy-loop, and the operational symptom is a connection pool full of clients hammering the server rather than anything that points at the cause.

So the usage pattern is pull, not long-poll. A worker asks, gets what is there or nothing, and decides for itself whether to sleep and ask again. That is fine for a batch-style worker, and it is a real constraint for anything that wants to sit on a connection waiting for a single job.

The other boundary is the one the protocol itself implies. There is no publish acknowledgement, no visibility timeout, no dead letter queue and no per-message metadata beyond the value and its expiry. The queue is a set of named lists of byte strings with cursor positions. Anything beyond that model is something you build on top, and the absence of those features is what makes the implementation small enough to read.

The maintenance question, and the licence ambiguity

There is a real discrepancy here that any adopter should notice.

The release list in the repository metadata ends at v0.6.2, dated 2016-01-30, with v0.6 from November 2015 and v0.5.1 before that. The last push was on 2026-05-23, and the repository is not archived. So there has been work on the branch in 2026, but the tagged releases stopped a decade ago. The README's own badge points at the latest release, and the telnet session in the documentation shows STAT version siberite-0.4.1, which is older still.

What that means practically is that there is no released artefact you can point at and know what is in it. Building from master is the realistic option, and then you are responsible for a codebase whose last tag predates the current Go toolchain by a long way.

The licence is the other thing to check before you adopt. The licence field on the repository reads Other, described as a custom licence GitHub cannot classify, and it points at the LICENSE file. The README's own licence link points at the Apache License 2.0 terms. Those two statements can be reconciled, since a LICENSE file that contains Apache 2.0 with a project-specific notice on top is still a custom file from GitHub's point of view, but you should open the file and read it rather than assume, particularly if you intend to redistribute a binary.

Against that, the codebase is small, in Go, and the protocol is one that already has clients. A team that wants a durable, disk-backed queue with cursor semantics and does not need a broker with acknowledgements can get a lot from this, and can read the whole implementation, which is the argument the README makes for Kestrel and Darner in the first place.

Editorial conclusion

Adopt Siberite if you need a queue that is larger than memory, that several consumers must read independently, or that has to survive a restart without a broker cluster to run. Do not adopt it if you need blocking reads, message acknowledgement, or a broker that other teams already know how to operate. Before you build on it, check the release age, since the last tagged release is v0.6.2 from 2016 and the last push was on 2026-05-23, and read the LICENSE file because the repository licence field shows a custom licence GitHub cannot classify.

Frequently asked questions

What is Siberite and how does it differ from Redis or RabbitMQ?

Siberite is a Go rewrite of Twitter's Kestrel and wavii's Darner that stores all messages out of process using goleveldb, rather than keeping them in memory like Redis or running as an enterprise broker like RabbitMQ. The result is a durable queue that uses minimal resident memory regardless of queue size.

What do durable cursors in Siberite do?

Using the syntax get queue.cursor_name initialises a durable cursor that advances with every read without deleting messages from the source queue, and there is no limit on the number of cursors per queue. A normal get queue still expires and deletes the returned message, and any cursor pointing at an expired message restarts from the queue head.

How do I build and start Siberite?

Fetch the source with go get, run the module fetch and then build siberite.go, create the data directory with mkdir ./data, and start the server with ./siberite -listen localhost:22133 -data ./data. Prebuilt darwin-x86_64 and linux-x86_64 builds are also on the releases page.

Does Siberite support blocking reads with a timeout?

No. The README lists waiting a given time limit for a new item to arrive, using the protocol's timeout syntax, as not supported: it is allowed by the protocol but does nothing. Consumers poll instead of long-polling.

How do I insert a message into several queues at once in Siberite?

Use the fanout syntax set queue+another_queue+third_queue, which inserts the message into all the named queues at once. The other listed feature is durable cursors, which let several consumers read the same queue independently without consuming each other's messages.

Official sources

  1. bogdanovich/siberite on GitHub
  2. Issues
  3. README
  4. 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/bogdanovich-siberite.svg)](https://hysenlabs.com/projects/bogdanovich-siberite)