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 readsdata[2]. - If
tail = 4, the next enqueue writesdata[4]. - After that enqueue,
tailbecomes(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:
frontstarts at 0rearstarts 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 == -1means 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 placedcount= number of valid elements currently in the buffer
With that:
- Empty is
count == 0. - Full is
count == capacity. headandtailare 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 < capacity0 <= tail < capacity0 <= 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 tocount - 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:
What it does
Extra space
—
—
Add to the rear if not full
O(1)
Remove from the front if not empty
O(1)
Read the front without removing
O(1)
count == capacity
O(1)
count == 0
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.
Traditional demo style
—
Global queue[], global front/rear
struct encapsulation printf("Overflow") inside enqueue
bool / error enum) int for indices
size_t for indices and sizes Manual print-and-look
Hard-coded 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
countfield. - 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_tfor indices/sizesboolfor clear success/failure- a
structto 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/destroypair
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, andcountassize_t. - I return
boolfor success/failure. - I require a valid
out_valuefor 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 |
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 |
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 |
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,
enqueuereturns 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), decrementcount - 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 < capacitytail < capacitycount <= 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 fullflag - Or reserve one slot and treat “full” as
(tail + 1) % capacity == head(capacity effectively becomescapacity - 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
% capacityand 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/isemptyfirst - 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.hfor the public APIcircular_queue.cfor 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_OKCQ_FULLCQ_EMPTYCQBADARG
Then callers can react differently to different failures.
3) Add policy-aware operations
If your app needs it, add explicit variants:
enqueuerejectnewenqueuedropoldest
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,countmean - 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.


