SPSCQueue: a wait-free single-producer single-consumer ring buffer for C++
A bounded single-producer single-consumer wait-free and lock-free queue written in C++11
At a glance
- What is it?
- SPSCQueue.h is a MIT licensed header-only C++11 queue that is wait-free and lock-free, built on a ring buffer with locally cached head and tail indices and explicit padding against false sharing, and it supports huge pages through the size-feedback allocator interface. It suits a producer and consumer pair on two threads, and it is the wrong structure the moment a second producer appears.
- Who is it for?
- SPSCQueue suits a pipeline with exactly one producer and one consumer, where a bounded queue is the handshake and the alternative is a mutex that shows up in a profile, because the implementation removes the lock and the cache coherency traffic that usually comes with it. It is a poor fit the moment you need a second producer, since the README states that any other usage is invalid, and a poor fit if you need unbounded or variable-capacity behaviour.
- 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 19 days ago.
- What is it written in?
- Mainly C++, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The API is deliberately small and the thread rule is absolute
The library is a single producer single consumer wait-free and lock-free fixed size queue written in C++11, header-only, with the README claiming it is faster than both boost::lockfree::spsc and folly::ProducerConsumerQueue. The API is a constructor taking a capacity that must be at least 1, a set of enqueue operations, and a small read side. The enqueue family covers emplace and push for copy and move construction, each with a try_ variant that returns false instead of blocking when the queue is full, and the move overloads participate in overload resolution only when std::is_constructible is satisfied for the target type. The read side is front, which returns a pointer to the front of the queue or nullptr if it is empty, and pop, which dequeues the first item. The usage rules are stated bluntly: only a single writer thread can enqueue and only a single reader thread can dequeue, and any other usage is invalid.
Blocking and non-blocking enqueues are two different programs
The distinction between the blocking and the try_ variants matters more than it first appears. push and emplace block if the queue is full, while try_push and try_emplace return true on success and false if the queue is full. That choice propagates into your consumer loop. The example in the README shows the blocking style, with a consumer thread that spins on front until an item appears.
SPSCQueue<int> q(1);
auto t = std::thread([&] {
while (!q.front());
std::cout << *q.front() << std::endl;
q.pop();
});
q.push(1);
t.join();The capacity of 1 in that example is deliberate, since it makes the queue full immediately, so the producer's push blocks until the consumer takes the item. In a real pipeline you would pick the try_ variants and handle a full queue as a signal, because a blocking producer on a bounded queue is a backpressure mechanism you have to design for rather than one you inherit. The pop method has a precondition the README states plainly, that you must ensure the queue is non-empty before calling it, which means front must have returned a non-nullptr before each pop, and it requires the element type to be nothrow destructible.
False sharing is handled by padding, not by hope
The implementation is a ring buffer, and the README is explicit about the false sharing problem and the fix. The head and tail indices are aligned and padded to the false sharing range, which is the cache line size, and the slots buffer is padded at the beginning and the end so it does not share a cache line with adjacent allocations. This is the part of the design that makes the difference between a queue that scales and one that does not, because two threads writing to two logically separate counters in the same cache line will bounce that line between cores on every operation. The README links the Intel article on avoiding and identifying false sharing among threads, the Wikipedia articles on ring buffer and false sharing, and includes a diagram of the memory layout in the repository as spsc.svg with an editable spsc.odg version.
Local index caching is the throughput trick
The second performance decision is that the writer and the reader each cache the index they do not own. The README walks through why, and the argument is short enough to follow. Without caching, a read must update the head index, which loads that cache line in exclusive state, and must read the tail index to check the queue is not empty, which loads that line in shared state. Since a write also needs to read the head index, the write is likely to require a coherency transition from shared back to exclusive, and in the worst case there is one such transition for every read and every write. Caching the tail index in the reader avoids stealing the writer's exclusive state, so a run of read operations completes without touching the writer's line, and the argument is symmetrical for the writer. The summary is that the caching reduces cache coherency traffic, which is where the throughput gain comes from.
Capacity is arbitrary, and the price is one wasted slot
There is a design decision that only shows up when you size the queue. The implementation allows arbitrary non-power-of-two capacities instead of allocating an extra queue slot to signal a full queue, which the README contrasts with other implementations that waste that slot. The tradeoff is stated in the other direction too: if you do not want to waste storage for an extra slot, you should use a different implementation. For most workloads a single wasted element in a ring buffer of thousands is irrelevant, but for a queue of two or three elements in a hot path it is not, and the README is upfront that this implementation is not the one for that case. The remaining state is reported by size, which returns the number of items currently available, and empty, which reports whether the queue is currently empty.
Huge pages need an allocator you supply, and the library declines to write it
Beyond the standard custom allocator interface, the library supports the size feedback proposal P0401R3, which lets an allocator report how much it actually allocated so that huge pages can be used without wasting space. Using size feedback requires C++17 to be enabled. The library does not include a huge page allocator, and the README gives two reasons, that the APIs for allocating huge pages are platform dependent and that handling huge page size and NUMA awareness is application specific. It then gives a Linux example anyway, an allocator that rounds the request up to a 2 MB multiple and uses mmap with MAP_HUGETLB, returning an AllocationResult with the pointer and the count, and a matching deallocate that calls munmap with the same rounding. The full example lives in src/SPSCQueueExampleHugepages.cpp, and the ordinary example is in src/SPSCQueueExample.cpp.
Testing a lock-free queue takes two approaches, and releases are rare
The README says testing lock-free algorithms is hard and describes two approaches. The first is a single threaded test that the functionality works as intended, including that the item constructor and destructor are invoked correctly, which is the part most queue implementations get wrong. The second is a multi-threaded test, and the section is cut off in the text available here, so the details of how the race is provoked are not described. The repository root is small and conventional, with include/ holding the header, src/ the examples, a CMakeLists.txt, a .clang-format file and a C and C++ continuous integration badge. Releases are infrequent, with v1.0 on 2020-04-17 and v1.1 on 2021-07-22, the latter noted as increased throughput, and the last push to the repository was on 2026-10-05, so the code is touched far more often than it is released.
Editorial conclusion
SPSCQueue suits a pipeline with exactly one producer and one consumer, where a bounded queue is the handshake and the alternative is a mutex that shows up in a profile, because the implementation removes the lock and the cache coherency traffic that usually comes with it. It is a poor fit the moment you need a second producer, since the README states that any other usage is invalid, and a poor fit if you need unbounded or variable-capacity behaviour. Verify first that your element type is nothrow destructible, which the pop method requires, and decide deliberately between the blocking and the try_ variants, because mixing a blocking push with a spin on front is a different program from one that handles a full queue explicitly.
Frequently asked questions
What is SPSCQueue used for?
It is a bounded single-producer single-consumer wait-free and lock-free fixed size queue written in C++11, intended to pass items between exactly one writer thread and one reader thread. The README states that any other usage is invalid.
What is the difference between push and try_push?
push blocks if the queue is full, while try_push returns true on success and false if the queue is full. Both come in copy and move flavours, and the move overloads participate in overload resolution only when the target type is constructible from the argument.
How does SPSCQueue avoid false sharing?
The head and tail indices are aligned and padded to the cache line size, and the slots buffer is padded at its beginning and end so it does not share a cache line with adjacent allocations. The writer and reader also cache the index they do not own, which reduces cache coherency traffic.
Does SPSCQueue support huge pages?
It supports the standard custom allocator interface and the P0401R3 size feedback proposal, which requires C++17. The library ships no huge page allocator, since the APIs are platform dependent, but the README gives a Linux mmap based example using MAP_HUGETLB.
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/rigtorp-spscqueue)Was this useful?