← The Ledger
Vol. I, No. 5 · Queue Structures

The Ring Buffer

A fixed-capacity queue implemented with two indices.

Written by Khushal Agrawal

loading…

Bounded Queues

A producer may generate events faster than a consumer can process them. An unbounded queue handles the difference by increasing memory usage.

A ring buffer uses a fixed allocation and requires an explicit policy for handling a full queue.

An Array and Two Indices

A ring buffer is an ordinary array with a tail for the next write and a head for the next read. Both begin at slot 0.

That is the whole representation: one allocation and two integers. Elements stay together in memory, with no linked nodes or per-item bookkeeping.

Push

To push an item, write it to the slot at the tail index and then increment the tail.

The operation does not shift existing elements or allocate memory. It runs in O(1) time and is suitable for latency-sensitive code such as interrupt handlers.

Pop

To pop an item, read the slot at the head index and then increment the head. The range between the head and tail represents the current queue contents.

A popped slot does not need to be physically cleared. Its value remains in memory until a later push overwrites it, while the indices determine whether the slot is logically occupied.

Wrap-Around

After an index reaches the final slot, its next position is slot 0: next = (i + 1) % capacity.

The array remains contiguous in memory, while modulo arithmetic treats its endpoints as adjacent. Freed slots can therefore be reused without resizing the array.

Full and Empty States

If wrapped head and tail indices are equal, the buffer may be either empty or full. Additional state is required to distinguish the two cases.

Common approaches store an explicit item count, reserve one unused slot, or use monotonically increasing indices and apply modulo only during array access. In the last approach, tail − head gives the item count.

Overwrite Mode

A full buffer may overwrite its oldest entry instead of blocking or rejecting a push. In this mode, the new item replaces the item at the head and both indices advance.

The buffer then retains the most recent N events. This policy is common in flight recorders, kernel logs, and diagnostic traces.

Power-of-Two Capacity

When capacity is a power of two, modulo can be replaced with a bit mask: (i + 1) & (capacity − 1).

This reduces index wrapping to a bitwise operation and allows monotonically increasing unsigned indices to wrap correctly across integer overflow.

Lock-Free

With one producer and one consumer, the ring buffer has a property most queues lack: each index has exactly one writer. The producer alone moves the tail; the consumer alone moves the head. Neither thread ever writes to the other's index.

That ownership split makes the single-producer single-consumer queue lock-free with only memory-ordering fences: the producer writes the element first and publishes the new tail second, so the consumer can never observe an index that points at unwritten data. This is the queue inside audio callbacks and network drivers, where taking a mutex is not an option.

Capacity Trade-offs

When a fixed-capacity buffer fills, the producer must block, drop the new item, or overwrite an old item. Capacity should account for expected workload bursts.

A bounded queue can provide backpressure instead of increasing memory use. Operations remain O(1), with no allocation after initialization.

From Kernels to Audio

Linux uses rings for its kernel log and exposes kfifo as a reusable primitive. io_uring shares submission and completion rings between user space and the kernel. Network cards use descriptor rings to hand packets to drivers running at a different pace.

Audio pipelines use single-producer, single-consumer rings between real-time and ordinary threads. The LMAX Disruptor applies the same shape to trading, while Rust’s VecDeque turns it into a growable deque.

Summary

A ring buffer uses head and tail indices to reuse a fixed array. Push and pop operations run in constant time without shifting elements.

Its fixed capacity makes memory use predictable and requires the application to define how overload is handled.

The Ring Buffer: A fixed-capacity queue implemented with two indices
The Ring Buffer — A fixed-capacity queue implemented with two indices.

Sources & further reading

Every claim in this chapter traces to a primary source. Peer-reviewed papers are linked by DOI.

Foundations

  1. Knuth, D. E. (1997). The Art of Computer Programming, Vol. 1, §2.2.2 — circular queues in sequential allocation. www-cs-faculty.stanford.edu/~knuth/taocp.html
  2. Ring buffer / circular buffer — overview and boundary conditions. en.wikipedia.org/wiki/Circular_buffer

Concurrency

  1. Thompson, M. et al. (2011). Disruptor: High performance alternative to bounded queues. LMAX technical paper. lmax-exchange.github.io/disruptor/disruptor.html
  2. Axboe, J. (2019). Efficient IO with io_uring. kernel.dk/io_uring.pdf

In practice

  1. Linux kernel — kfifo, the generic in-kernel ring buffer. www.kernel.org/doc/html/latest/core-api/kfifo.html
  2. Rust standard library — VecDeque, a ring-buffer-backed deque. doc.rust-lang.org/std/collections/struct.VecDeque.html
  3. JUCE audio framework — AbstractFifo for real-time audio handoff. docs.juce.com/master/classAbstractFifo.html

Enjoyed this chapter? Support The Ledger.