What is a circular queue and why is it used?
Updated Feb 20, 2026
Short answer
A circular queue is a fixed-size array whose front and rear indices wrap around to the start when they reach the end, using modulo arithmetic. It exists because a naive array queue leaks space — after repeated dequeues the front drifts rightward and the vacated slots at the beginning are unreachable. Wrapping reclaims them, giving O(1) enqueue and dequeue with no shifting and no reallocation.
Deep explanation
In a linear array queue, dequeuing advances front and leaves a hole:
after 3 dequeues: [ _ ][ _ ][ _ ][ D ][ E ][ ][ ] ^front ^rearThose three slots are wasted. You either shift everything left on each dequeue — O(n) — or let the queue report "full" while a third of it sits empty. Circular indexing solves both:
class CircularQueue: def __init__(self, capacity): self.buf = [None] * capacity self.front = self.size = 0 self.cap = capacity
def enqueue(self, x): if self.size == self.cap: raise OverflowError('queue is full') self.buf[(self.front + self.size) % self.cap] = x self.size += 1
def dequeue(self): if self.size == 0: raise IndexError('queue is empty') x = self.buf[self.front] self.buf[self.front] = None # release the reference self.front = (self.front + 1) % self.cap self.size -= 1 return xTracking an explicit size sidesteps the classic ambiguity: with only front and rear, the full and empty states both satisfy front == rear. The alternatives are to keep a count (as above) or to deliberately waste one slot so full means (rear + 1) % cap == front.
Why it matters in practice. Fixed capacity is a feature, not a limitation — it bounds memory, so a producer that outruns its consumer fails predictably instead of exhausting the heap. That is why circular buffers underpin audio and video pipelines, network packet buffers, embedded firmware, and ring-buffer loggers.
Real-world example
An audio driver holds the last 4,096 samples while playback consumes them:
ring = CircularQueue(4096)
def on_sample(sample): # producer, called at 48 kHz if ring.size == ring.cap: ring.dequeue() # overwrite the oldest — drop, never block ring.enqueue(sample)Memory is constant regardless of runtime. An unbounded queue here would grow without limit whenever playback fell behind, and eventually take down the process — in audio, dropping the oldest sample is far better than a crash.
Common mistakes
- - Distinguishing full from empty using `front == rear` alone
- both states satisfy it. Track a size or sacrifice one slot.
- - Omitting the modulo, so indices run past the end of the array.
- - Failing to clear the dequeued slot in a garbage-collected language, which keeps the object alive and leaks memory.
- - Assuming it can grow. Resizing a circular buffer means allocating and re-linearising — if you need unbounded growth, this is the wrong structure.
- - Overlooking the overwrite-versus-block decision when full
- the two behaviours suit very different applications.
Follow-up questions
- How do you distinguish a full queue from an empty one?
- What happens when a circular queue fills up?
- How does this relate to a ring buffer in systems programming?
- Why is fixed capacity considered an advantage?