C Program to Implement a Circular Queue (Ring Buffer) in 2026

You know that moment when you build a simple queue with an array, everything works, and then—after a few dequeues—you realize the “front” of the array is full of unused space you can’t reuse without shifting elements? In small demos that shift is annoying; in real code it’s a performance and complexity tax you pay forever.

A circular queue fixes that by treating the array like a loop (a ring). When the rear reaches the end, it wraps back to index 0. You keep FIFO behavior, you keep O(1) operations, and you stop wasting space at the front.

In this post I’m going to show you how I implement an array-based circular queue in C in a way that’s actually pleasant to maintain in 2026: clear invariants, no “magic” corner cases, and a complete runnable program you can drop into a project. I’ll also call out the mistakes I see most often—especially around “full vs empty” detection—and how to avoid debugging sessions that feel like chasing ghosts.

Circular queue as a ring buffer: the mental model that makes it easy

Think of a circular queue as a checkout line painted on a circular track.

  • The buffer is a fixed-size array.
  • The front (head) points to the next element that will leave.
  • The rear (tail) points to where the next element will be inserted.
  • Both pointers move forward, and when they hit the end, they wrap back to the beginning.

That wrap is the entire trick:

  • Moving forward is index = (index + 1) % capacity.
  • No shifting. No compaction. No chasing pointers.

From a performance standpoint, this structure behaves like a tiny, hot ring of memory. With an array, you get predictable cache behavior and constant-time work per operation.

Where people get stuck is not the wrap—it’s the bookkeeping. You have to answer two questions quickly and correctly:

  • Is the queue empty? (Nothing to dequeue or peek.)
  • Is the queue full? (No space to enqueue.)

If you don’t design those checks cleanly, every operation turns into “if this then that” special cases.

A quick concrete picture (indices on a ring)

When I teach this to myself (yes, I still do), I like to imagine the array indices laid out in a circle. For capacity 5, indices are 0 1 2 3 4, but conceptually 4 points back to 0.

  • If head = 2, the next dequeue reads data[2].
  • If tail = 4, the next enqueue writes data[4].
  • After that enqueue, tail becomes (4 + 1) % 5 = 0.

That’s the wrap-around in one line.

Why a plain array queue wastes space (and why shifting is the wrong fix)

A simple array queue often starts like this:

  • front starts at 0
  • rear starts at -1 (or 0, depending)
  • enqueue increments rear
  • dequeue increments front

After a few dequeues, the “active” queue elements live somewhere in the middle of the array. The unused area at the front is dead space.

The naive “fix” is to shift everything left after each dequeue (or when front gets too large). That works, but it’s expensive:

  • Every shift is O(n) work.
  • Shifting makes performance unpredictable (sometimes O(1), sometimes O(n)).
  • Shifting is bug-friendly: off-by-one mistakes, overwritten elements, and half-shifted states when errors occur.

Circular queues solve the root problem: reuse storage without moving existing elements.

State and invariants: choosing a representation that won’t bite you later

You can represent a circular queue in a few ways. Two common approaches are:

  • Sentinel indices (often front == -1 means empty) and arithmetic to detect full.
  • Explicit count (track how many items are currently stored).

In 2026, when I’m writing C that I expect to live longer than a weekend, I strongly prefer the explicit count approach. It makes the invariants easy to state and easy to test.

Here’s the version I recommend:

  • head = index of the front element (the next to be dequeued)
  • tail = index where the next enqueued element will be placed
  • count = number of valid elements currently in the buffer

With that:

  • Empty is count == 0.
  • Full is count == capacity.
  • head and tail are always in [0, capacity).

That’s it. No ambiguity. No “but what if head == tail?” debates.

The invariants I keep in my head (and sometimes write in comments)

If you’re debugging a circular queue, the fastest path is usually: “Are my invariants still true?” These are the ones I care about:

  • 0 <= head < capacity
  • 0 <= tail < capacity
  • 0 <= count <= capacity
  • If count == 0, then the queue is empty and dequeue/peek must fail.
  • If count == capacity, then the queue is full and enqueue must fail.
  • The logical order of elements is: data[(head + 0) % capacity], data[(head + 1) % capacity], … up to count - 1.

When those stay true, everything else tends to fall into place.

Operations and costs

For an array-backed circular queue with count tracking, the basic operations are constant time:

Operation

What it does

Time

Extra space

Enqueue

Add to the rear if not full

O(1)

O(1)

Dequeue

Remove from the front if not empty

O(1)

O(1)

Peek

Read the front without removing

O(1)

O(1)

IsFull

count == capacity

O(1)

O(1)

IsEmpty

count == 0

O(1)

O(1)### Traditional vs modern C patterns (what I actually ship)

I still see a lot of examples that use global variables and print inside data-structure functions. That’s fine for a quick console demo, but it’s not how you want to structure library code.

Topic

Traditional demo style

Modern, maintainable style —

— State

Global queue[], global front/rear

struct encapsulation Errors

printf("Overflow") inside enqueue

Return status (bool / error enum) Types

int for indices

size_t for indices and sizes Testing

Manual print-and-look

Assertions + sanitizers + small tests Reuse

Hard-coded capacity

Compile-time constant or passed-in capacity

In the runnable program below, I keep printing in main() and keep queue functions pure: they return a status and (when needed) write the result to an output parameter.

Full vs empty: three clean ways to design it

Even though I recommend count, it’s worth understanding the alternatives because you’ll see them in existing codebases.

Option A: Track count (my default)

  • Pros: unambiguous, easy to reason about, easy to test.
  • Cons: one more field to maintain (but you must maintain something anyway).

Option B: Keep a separate full flag

You track head, tail, and a bool full. Now head == tail means either empty or full, and the flag disambiguates.

  • Pros: avoids count, some people prefer it for byte ring buffers.
  • Cons: you now have a flag that must be updated perfectly on every operation; if it falls out of sync, debugging is painful.

Option C: Reserve one slot (capacity effectively becomes capacity - 1)

You declare the queue “full” when advancing tail would collide with head:

  • full if (tail + 1) % capacity == head
  • empty if tail == head
  • Pros: no count field.
  • Cons: you lose one slot of storage; a “size 5” buffer holds only 4 items.

In practice: if you need every slot, use count or full. If you want minimal fields and can afford the lost slot, the reserved-slot method is a classic.

Operations with real edge cases (the ones that break naive code)

Before code, I like to walk through the edge cases that define correctness.

Enqueue

Rules I follow:

  • If the queue is full, enqueue must fail without modifying state.
  • Otherwise:

– write to buffer[tail]

– advance tail

– increment count

The “fail without modifying state” part matters. If you update tail and then detect full, you’ve corrupted the queue.

A habit I’ve built: precondition check first, then do the write, then do the state updates. The order keeps bugs obvious.

Dequeue

Rules I follow:

  • If the queue is empty, dequeue must fail without modifying state.
  • Otherwise:

– read from buffer[head]

– advance head

– decrement count

Notice that, like enqueue, state updates come after you successfully read. That makes it harder to “lose” an element if you later add logging, metrics, or error handling.

Peek

Peek should not change anything. If it does, you’ve implemented dequeue.

I also treat peek as a first-class operation because it makes application code cleaner. Without peek, callers sometimes fake it by dequeue-then-enqueue, which is both slower and more error-prone.

Wrap-around scenario (the one you should test first)

If capacity is 5, a good sanity sequence is:

  • Enqueue 5 items (queue becomes full)
  • Dequeue 2 items (space opens at the front)
  • Enqueue 2 more items (tail wraps to index 0)

If your display function or full/empty logic is wrong, this is where it shows up.

Another edge case that catches bugs: alternating operations

A lot of broken implementations “work” when you do a big block of enqueues followed by a big block of dequeues. They fail when you alternate:

  • enqueue, dequeue, enqueue, dequeue, …

Alternation stresses the invariants because head and tail move constantly and wrap frequently.

Complete C program: array-based circular queue (runnable)

This is a full program you can compile and run. It uses:

  • size_t for indices/sizes
  • bool for clear success/failure
  • a struct to keep state together
  • no printing inside queue operations (so you can reuse it in other contexts)

#include

#include

#include

#define CQ_CAPACITY 5

typedef struct {

int data[CQ_CAPACITY];

size_t head; // index of the front element

size_t tail; // index where the next element will be inserted

size_t count; // number of stored elements

} CircularQueue;

static void cq_init(CircularQueue *q) {

q->head = 0;

q->tail = 0;

q->count = 0;

}

static bool cqisempty(const CircularQueue *q) {

return q->count == 0;

}

static bool cqisfull(const CircularQueue *q) {

return q->count == CQ_CAPACITY;

}

static bool cq_enqueue(CircularQueue *q, int value) {

if (cqisfull(q)) {

return false;

}

q->data[q->tail] = value;

q->tail = (q->tail + 1) % CQ_CAPACITY;

q->count++;

return true;

}

static bool cqdequeue(CircularQueue q, int outvalue) {

if (cqisempty(q)) {

return false;

}

if (out_value != NULL) {

*out_value = q->data[q->head];

}

q->head = (q->head + 1) % CQ_CAPACITY;

q->count–;

return true;

}

static bool cqpeek(const CircularQueue q, int outvalue) {

if (cqisempty(q)) {

return false;

}

if (out_value != NULL) {

*out_value = q->data[q->head];

}

return true;

}

static void cq_print(const CircularQueue *q) {

if (cqisempty(q)) {

puts("Queue: (empty)");

return;

}

printf("Queue: ");

for (size_t i = 0; i count; i++) {

sizet index = (q->head + i) % CQCAPACITY;

printf("%d ", q->data[index]);

}

putchar(‘\n‘);

printf("State: head=%zu tail=%zu count=%zu\n", q->head, q->tail, q->count);

}

int main(void) {

CircularQueue q;

cq_init(&q);

puts("Enqueue 101, 102, 103, 104, 105");

for (int v = 101; v <= 105; v++) {

if (!cq_enqueue(&q, v)) {

printf("Enqueue failed for %d (full)\n", v);

}

}

cq_print(&q);

puts("Dequeue two items");

for (int i = 0; i < 2; i++) {

int value;

if (cq_dequeue(&q, &value)) {

printf("Dequeued: %d\n", value);

} else {

puts("Dequeue failed (empty)");

}

}

cq_print(&q);

puts("Enqueue 201 and 202 (forces wrap-around)");

if (!cq_enqueue(&q, 201)) {

puts("Enqueue failed for 201 (full)");

}

if (!cq_enqueue(&q, 202)) {

puts("Enqueue failed for 202 (full)");

}

cq_print(&q);

puts("Peek at front");

{

int front;

if (cq_peek(&q, &front)) {

printf("Front is: %d\n", front);

} else {

puts("Peek failed (empty)");

}

}

puts("Drain the queue");

while (!cqisempty(&q)) {

int value;

cq_dequeue(&q, &value);

printf("Dequeued: %d\n", value);

}

cq_print(&q);

puts("Try dequeue on empty");

{

int value;

if (!cq_dequeue(&q, &value)) {

puts("Dequeue failed (empty) as expected");

}

}

return 0;

}

How to compile

If you have clang or gcc, this is enough:

  • clang -std=c17 -Wall -Wextra -Wpedantic circularqueue.c -o circularqueue
  • ./circular_queue

If you want quick safety checks while you iterate, I typically add sanitizers during development:

  • clang -std=c17 -Wall -Wextra -Wpedantic -fsanitize=address,undefined -fno-omit-frame-pointer circularqueue.c -o circularqueue

That catches out-of-bounds, use-after-free (not relevant here), and undefined behavior issues early.

Making it more reusable: capacity as a field + heap allocation

The example above is deliberately compact and uses a compile-time capacity. That’s great for learning and for embedded-ish contexts where sizes are known.

In application code, I often want:

  • the same queue type with different capacities
  • a queue allocated on the heap
  • a clean init / destroy pair

Here’s the shape I reach for. I’m still keeping it “C simple,” but now capacity is not a macro.

Design notes:

  • I keep head, tail, and count as size_t.
  • I return bool for success/failure.
  • I require a valid out_value for dequeue/peek in library mode (you can relax that if you want).

#include

#include

#include

typedef struct {

int *data;

size_t capacity;

size_t head;

size_t tail;

size_t count;

} IntCircularQueue;

static bool icqinit(IntCircularQueue *q, sizet capacity) {

if (q == NULL || capacity == 0) {

return false;

}

q->data = (int )malloc(sizeof(int) capacity);

if (q->data == NULL) {

return false;

}

q->capacity = capacity;

q->head = 0;

q->tail = 0;

q->count = 0;

return true;

}

static void icq_destroy(IntCircularQueue *q) {

if (q == NULL) {

return;

}

free(q->data);

q->data = NULL;

q->capacity = 0;

q->head = 0;

q->tail = 0;

q->count = 0;

}

static bool icqisempty(const IntCircularQueue *q) {

return q == NULL || q->count == 0;

}

static bool icqisfull(const IntCircularQueue *q) {

return q != NULL && q->count == q->capacity;

}

static bool icq_enqueue(IntCircularQueue *q, int value) {

if (q == NULL |

q->data == NULL

icqisfull(q)) {

return false;

}

q->data[q->tail] = value;

q->tail = (q->tail + 1) % q->capacity;

q->count++;

return true;

}

static bool icqdequeue(IntCircularQueue q, int outvalue) {

if (q == NULL |

q->data == NULL outvalue == NULL

icqis_empty(q)) {

return false;

}

*out_value = q->data[q->head];

q->head = (q->head + 1) % q->capacity;

q->count–;

return true;

}

static bool icqpeek(const IntCircularQueue q, int outvalue) {

if (q == NULL |

q->data == NULL outvalue == NULL

icqis_empty(q)) {

return false;

}

*out_value = q->data[q->head];

return true;

}

This “heap-backed” version is still a circular queue, still O(1), but it’s much easier to drop into a real project because capacity isn’t a compile-time decision.

Choosing a capacity (the practical guidance I actually use)

Capacity choice is one of those unglamorous decisions that decides whether your queue is a hero or a hidden bug.

Here’s how I think about it:

  • If overflow must never happen (for example, you’re buffering critical work items), you either need a capacity large enough for worst-case bursts or you need a different design (like blocking producers, spilling to disk, or dynamically growing).
  • If overflow is acceptable (for example, debug logs, telemetry, “latest samples”), a fixed-size circular queue is perfect. When it’s full, you can choose to reject new data or drop old data.

Two overflow policies: reject-new vs drop-old

The code above implements “reject-new”:

  • when full, enqueue returns false and does nothing

Sometimes you want the opposite: keep the most recent values and discard the oldest. That’s a legitimate policy for metrics, smoothing windows, and “last N events” buffers.

To implement “drop-old,” you can do:

  • if full, advance head (discard oldest), decrement count
  • then perform the normal enqueue

I don’t mix policies silently. I name the function differently (for example cqenqueuedrop_oldest) so callers know the semantics.

Powers of two (optional micro-optimization)

If capacity is a power of two, you can replace modulo with a bitmask:

  • index = (index + 1) & (capacity - 1)

This can be slightly faster in tight loops. But I only do this if I’ve measured a need, because:

  • it restricts allowed capacities
  • it makes the code a bit less “obvious”

For most queues in application code, the clean % capacity version is already the right trade.

Debugging a circular queue without losing your mind

Circular queues are small, but they can fail in ways that feel weird if you print the wrong thing.

Print logical order, not physical storage

The easiest debugging mistake is to print the backing array from 0..capacity-1 and assume it reflects the queue content. It doesn’t, especially after wrap-around.

When I debug, I print two views:

  • Logical order (what dequeue will return)
  • Raw array (what’s physically stored, even if stale)

A raw array can contain old values that are no longer in the queue. That’s not a bug. The count is what tells you what’s valid.

A tiny “dump state” function

If you’re tracking a bug, a state dump like this is gold:

  • head, tail, count
  • capacity
  • the logical sequence of elements

You already have cq_print() in the sample program doing the logical sequence correctly.

If you expand to a heap-backed version, I like adding a debug-only check that asserts invariants:

  • head < capacity
  • tail < capacity
  • count <= capacity

In C, a single assert() at the top of enqueue() and dequeue() can save you hours.

Common mistakes I see (and quick fixes)

Circular queues are small, but they’re famous for off-by-one and state bugs. Here are the failures I run into most often.

Mistake 1: Confusing the full and empty conditions

If you only track head and tail, the condition head == tail is ambiguous:

  • It can mean empty (no elements)
  • It can also mean full (all slots filled and tail wrapped)

Fix options:

  • Track count (what I did above)
  • Or keep a separate bool full flag
  • Or reserve one slot and treat “full” as (tail + 1) % capacity == head (capacity effectively becomes capacity - 1)

For most application code, count is the least surprising.

Mistake 2: Updating indices before checking preconditions

A subtle bug pattern:

  • advance tail
  • write to buffer
  • then detect “oh, it was full”

Now you’ve overwritten valid data and moved state forward. Always check full/empty first, then mutate state.

Mistake 3: Display logic that assumes a linear array segment

A circular queue’s contents might wrap. If your display() prints data[head..tail] and head > tail, you’ll print garbage or miss elements.

Fix: iterate count elements from head, wrapping with modulo, exactly like cq_print() does.

Mistake 4: Mixing signed and unsigned indices

In older samples, front = -1 is a common “empty” marker. That forces front to be signed, and then you mix it with sizes and modulo arithmetic. That’s where warnings appear—and where real bugs hide.

Fix: use size_t and count, or use a dedicated sentinel representation consistently (and keep the arithmetic clean).

Mistake 5: Printing from inside the queue library functions

This isn’t a correctness bug, but it becomes a maintenance bug.

If you print inside enqueue(), you can’t reuse the queue in:

  • a service that logs differently
  • a unit test
  • a UI app

Fix: return a status, and let the caller decide how to report errors.

Mistake 6: Not defining what happens on overflow/underflow

Some demos print “Overflow” and keep going. Real software needs a decision:

  • On overflow: reject new data, drop old data, or block/wait.
  • On underflow: return failure cleanly.

If you don’t define the policy, different callers will assume different things, and you’ll get bugs that look like “missing messages” or “random duplicates.”

Mistake 7: Forgetting that old values can remain in the array

Even with correct logic, the backing array still contains old integers from earlier operations. If you inspect memory in a debugger and assume every non-zero value is “in the queue,” you’ll be misled.

The queue’s truth is head, tail, and count—not the raw contents of the array.

Practical scenarios where a circular queue is the right tool

I reach for a circular queue (ring buffer) when I need:

  • Fixed memory usage: I know exactly how much RAM I can spend.
  • Predictable performance: enqueue/dequeue must be O(1).
  • FIFO semantics: old items leave before new items.

Some real scenarios:

1) Input buffering (serial, socket, file reads)

If you’re reading data in bursts, a ring buffer is a natural fit. You can enqueue bytes as they arrive and dequeue them as your parser consumes them.

Even if you ultimately build a higher-level protocol, the ring buffer is a great low-level primitive.

2) Producer/consumer pipelines inside a single process

If one part of your program produces work and another consumes it, a queue makes the boundary explicit. In a single-threaded loop, this can be as simple as:

  • poll inputs
  • enqueue tasks
  • dequeue and execute tasks

3) “Last N samples” and smoothing

If you want a rolling window (for example, the last 60 sensor readings), a circular queue is almost perfect. Pair it with a running sum and you can compute a moving average efficiently.

4) Logging buffers

Sometimes you want to keep the last N log entries in memory so you can dump them when something fails. In that case, the “drop-oldest” enqueue policy is often what you want.

When I would not use a circular queue

Circular queues are great, but they’re not a universal replacement for other structures.

I avoid them when:

  • The queue must grow without bound: use a linked structure or a dynamically resized buffer.
  • You need random removal (not FIFO): use a different container.
  • You need priority ordering: use a heap/priority queue.
  • You need multi-producer/multi-consumer thread safety and don’t want locks: now you’re in a more advanced territory (lock-free queues are possible, but they’re not a “tiny data structure” anymore).

Also: if you’re constantly hitting “full,” that’s not a queue bug—it’s an architecture signal. Either consumers can’t keep up, or capacity is too small for the workload.

Performance considerations (what matters, what doesn’t)

Most circular queue implementations are fast enough, but here are the performance points that actually matter in practice.

Cache friendliness

Array-backed circular queues are cache-friendly because:

  • elements are stored contiguously
  • the working set is small and reused

Compared to a linked-list queue, you avoid pointer chasing and scattered allocations.

Modulo cost

The modulo operation can be a small cost in very tight loops. Options:

  • Keep % capacity and write clear code (my default).
  • Use power-of-two capacity and bitmasking if you’ve measured it matters.

Branching and error paths

If most operations succeed, keep the “failure” path short and early:

  • check isfull/isempty first
  • return quickly

That keeps the happy path simple and predictable.

Copy cost depends on element type

With int, copies are trivial. With a large struct, copying into the queue is the real cost.

If you store big items, consider storing pointers (with clear ownership rules), or storing indices/handles.

Thread-safety (a quick reality check)

The sample code is not thread-safe. That’s intentional: correctness and clarity first.

If you need thread safety, you have choices:

  • Add a mutex around enqueue/dequeue.
  • Use a single-producer/single-consumer ring buffer design with careful atomic operations.

I don’t casually “sprinkle atomics” on a queue. Concurrency needs a deliberate design and tests that stress it.

If you’re staying single-threaded (and many programs should), you can ignore this entire section and enjoy simple code.

What I’d do next in a real codebase

If you’re using a circular queue as more than a classroom exercise, I’d push it one notch further: make it generic enough for your needs and easy to verify.

First, I’d decide whether the queue needs to store ints, structs, or bytes. For many systems tasks (I/O buffering, telemetry, message passing), a byte ring buffer is the right primitive, and you can layer parsing on top. For application work, a typed queue of structs often reads better.

Second, I’d add a tiny test harness that hammers wrap-around cases. My favorite approach is to generate random sequences of enqueue/dequeue operations, mirror the expected behavior in a simple reference model (even a plain array with shifting is fine for tests), and assert that the results match. That kind of test catches “rare” corner bugs fast.

Third, I’d compile with warnings maxed out and run with sanitizers while I develop. In my experience, that shortens feedback loops more than any amount of careful staring at index arithmetic.

Finally, I’d document the invariants right next to the struct definition: what head, tail, and count mean, and which functions are allowed to mutate them. Once you do that, circular queues stop feeling tricky—they become one of the most dependable little tools in a C programmer’s kit.

Expansion Strategy

If you want to expand this into a small “drop-in module” you can reuse across projects, here’s the strategy I follow.

1) Split interface and implementation

I typically create:

  • circular_queue.h for the public API
  • circular_queue.c for the implementation

That gives you:

  • a clean separation between “what it does” and “how it does it”
  • easy reuse without copy/pasting entire programs

2) Decide on an error model up front

For small queues, bool is fine. If you want better diagnostics, define an enum:

  • CQ_OK
  • CQ_FULL
  • CQ_EMPTY
  • CQBADARG

Then callers can react differently to different failures.

3) Add policy-aware operations

If your app needs it, add explicit variants:

  • enqueuerejectnew
  • enqueuedropoldest

I don’t hide behavior behind a flag unless the codebase already has that pattern.

4) Add invariant checks in debug builds

In debug mode, assert the invariants at the top of each operation. You’ll catch mistakes close to the source.

5) Write tests that target wrap-around

Don’t just test “enqueue N, dequeue N.” Test:

  • wrap-around after partial dequeues
  • alternation patterns
  • boundary conditions: capacity 1, capacity 2, small sizes that make off-by-one bugs obvious

If Relevant to Topic

If you’re putting a circular queue into production code (or anything long-lived), a few extra considerations pay off.

Observability: measure drops and failures

If the queue can overflow, I like to count:

  • how many enqueue attempts failed due to full
  • how many items were dropped (if using drop-oldest)

Those counters turn “mysterious missing data” into “we hit capacity at 14:03.”

Tooling: static analysis and UB avoidance

Beyond sanitizers, it’s worth running a static analyzer occasionally. Even for a small data structure, it can catch:

  • missing null checks
  • incorrect size calculations
  • inconsistent initialization

Documentation: write down the invariants and policy

If someone else touches the code later, the most valuable comment is not “this increments tail.”

It’s:

  • what head, tail, count mean
  • what constitutes full/empty
  • what enqueue does on full

That small bit of documentation prevents “helpful refactors” that quietly break correctness.

If you take nothing else from this: a circular queue is easy when you commit to a representation with clear invariants. Once count defines full/empty unambiguously, the implementation becomes simple enough that you can trust it—and simple enough that future-you won’t dread maintaining it.

Scroll to Top