softwareengineering softwarearchitecture

Ring Buffer Data Structure

A ring buffer(circular buffer) ia a fixed-size data structure that reuses its storage by wrapping around to the beginning when it reaches the end.

It’s like an array whose end connects logically to its beginning

Physically, the memory is not arranged in a circle. It is a normal contiguous array. The cicular behavior comes from how we calculate the next memory position.

Internal Structure

A simple ring buffer needs four main pieces of information.

ComponentPurpose
BufferFixed-capacity storage
CapacityMaximum number of elements
Write positionWhere the next element is inserted
Read positionWhere the oldest unread element is located
type RingBuffer struct {
    data  []int
    head  int // Next read position
    tail  int // Next write position
    count int // Number of stored elements
}

The crucial observation is that the read and write positions move independently.

  • Writing moves the write position forward.
  • Reading moves the read position forward.
  • When either reaches the end, it wraps around.
  • The storage capacity never changes.

Important Concept - Modulo

A normal array moves through indexes like this: 0 → 1 → 2 → 3 → 4 → out of bounds

But a ring buffer needs: 0 → 1 → 2 → 3 → 4 → 0 → 1 ...

We achieve this using the modulo operator:

For a capacity of 5:

Current indexCalculationNext index
0(0 + 1) % 51
1(1 + 1) % 52
2(2 + 1) % 53
3(3 + 1) % 54
4(4 + 1) % 50

What happens when the buffer is full ?

There are different policies to handle this.

PolicyBehavior when full
Overwrite oldestOldest unread data is lost; newest is kept
Reject new dataNew item is dropped; existing data remains
Block or waitProducer waits for the consumer to make space

eBPF Ring Buffer Implementation.

eBPF uses a ring buffer to efficiently transfer events (process execution, network activity, etc.) from kernel space to userspace, where a Go application can process them.

The Linux BPF_MAP_TYPE_RINGBUF uses the second approach. When space is insufficient, reservation fails rather than overwriting unread events.

How it works:

  1. An eBPF program detects an event.
  2. It reserves memory in the ring buffer.
  3. It writes event data into the reserved space.
  4. It submits the event, making it available for reading.
  5. A Go application reads and processes the event.
  6. The consumed memory becomes reusable.

Important concepts:

  • MPSC: Multiple producers (eBPF programs running on different CPUs), single consumer (userspace reader).
  • Fixed capacity: The buffer doesn’t automatically grow. If there’s insufficient space, a reservation fails rather than overwriting unread events.
  • Variable-length records: Events can occupy different amounts of memory.

5 reasons ring buffers work well for eBPF

  1. Fast Operations — O(1): Quickly insert and read events without searching through existing data. (It already knows the next write position.)
  2. Fixed Memory Usage: Uses preallocated, bounded memory, preventing uncontrolled memory growth in the kernel.
  3. Efficient Kernel-to-Userspace Communication: Uses shared, memory-mapped buffers to transfer events with minimal copying and overhead.
  4. Non-Blocking Event Production: The kernel doesn’t wait for userspace to consume events. If the buffer is full, new reservations fail instead of blocking.

Ring buffer vs other approaches

FeatureBPF ring bufferBPF hash mapBPF perf buffer
Main purposeEvent streamingStore keyed stateEvent streaming
Shared across CPUsYes, typicallyDepends on map typeUsually per-CPU buffers
Cross-CPU event orderingReservation orderNot a FIFO streamNot guaranteed globally
Memory boundedYesYesYes
Best use caseProcess/network eventsCounters, lookup tablesPer-CPU event streams

The Linux kernel documentation specifically identifies shared memory efficiency and cross-CPU event ordering as the two main reasons the BPF ring buffer was introduced.
BPF ring buffer — The Linux Kernel documentation

Tip

But they’re not always the best solution. For example, if you only want to count packets by IP address, a BPF hash map may be better because you don’t need to transfer every packet event to Go.

The useful distinction is:

  • Ring buffer: “Tell my Go program what happened.”
  • Hash map: “Keep track of information inside the kernel.”