Circular lock-free buffer
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
A circular lock-free buffer, usually called a lock-free ring buffer, is a fixed-size queue designed for fast communication between threads. The simplest and most practical version is a single-producer, single-consumer buffer, where one thread writes items and one thread reads them using atomic head and tail indices instead of a mutex.
Start With The Ring Buffer Idea
A circular buffer stores items in a fixed array. When the writer reaches the end, it wraps around to index zero again.
The queue state is usually tracked with two counters:
- '
head, where the consumer reads next' - '
tail, where the producer writes next'
If head == tail, the buffer is empty. If advancing tail would make it equal to head, the buffer is full.
Why Lock-Free Is Harder Than It Sounds
Lock-free does not mean "no coordination." It means the coordination happens through atomic operations and memory ordering rather than a traditional lock.
A correct design still has to answer:
- who owns each index update
- when written data becomes visible to the other thread
- how full and empty states are detected safely
These details are manageable in the single-producer, single-consumer case. They become much more complex for multiple producers or multiple consumers.
A Practical SPSC Example In C++
This example is intentionally limited to one producer and one consumer. That limitation is what keeps the ownership model simple enough to reason about.
Memory Ordering Matters
The producer must publish the written element before publishing the updated tail. The consumer must observe the updated tail before reading the element. That is why the sample uses acquire and release semantics instead of only relaxed operations.
If you get the ordering wrong, the code may appear to work on one machine and fail on another under load.
Full Versus Empty Detection
A classic design leaves one slot unused so that full and empty states are easy to distinguish. That means a buffer declared with capacity N can store at most N - 1 elements.
There are other ways to track fullness, such as maintaining a separate count, but the one-slot-empty rule keeps the state logic simpler and common in real implementations.
Why MPMC Is A Different Problem
A multi-producer or multi-consumer ring buffer usually needs per-slot sequence numbers, compare-and-swap loops, or another stronger coordination design. Simply making both head and tail atomic is not enough for that case.
That is why most discussions should say explicitly whether the buffer is:
- SPSC
- MPSC
- SPMC
- MPMC
Those are different algorithms, not just minor variations.
Where Ring Buffers Shine
Lock-free circular buffers are useful in logging pipelines, audio processing, telemetry, networking, and any system where fixed-capacity, low-latency message passing is more important than unbounded growth.
The fixed size is a feature, not a limitation, when you care about predictable memory use.
Common Pitfalls
The most common mistake is calling a data structure "lock-free" when it is only safe for one producer and one consumer but is then used by multiple writers. Another is ignoring memory ordering and relying on relaxed atomics everywhere. Developers also often forget that many ring buffers intentionally waste one slot to distinguish full from empty. Finally, a fixed-capacity queue needs an overflow policy; if push fails, the caller must know whether to drop, retry, or block elsewhere.
Summary
- A lock-free circular buffer is usually simplest and safest in the SPSC case.
- Atomic indices replace a mutex, but memory ordering still matters.
- Full and empty detection are core design choices, not afterthoughts.
- MPMC ring buffers are a substantially harder algorithmic problem.
- Fixed capacity gives predictable performance, but the caller still needs a clear overflow strategy.

