Queues
Reading time: 25 minutes
A queue is a linear data structure that follows the First In, First Out (FIFO) principle. Elements enter at one end (the back) and exit from the other end (the front), just like a line of people waiting at a checkout counter. The person who arrives first gets served first, and new arrivals join at the back.
Queues are everywhere in computing: operating systems use them to schedule tasks, web servers use them to handle incoming requests, and printers use them to manage print jobs. Understanding queues deeply helps you recognize when they are the right tool and how to implement them efficiently. This page covers queue operations, multiple implementation strategies, specialized variants like circular queues and deques, and real-world applications where queues excel.
The FIFO principle
The First In, First Out principle is the defining characteristic of a queue. Unlike a stack where the most recent item comes out first, a queue processes items in the order they arrived.
Consider a print queue. When you send three documents to a printer:
Document A arrives first and enters the queue
Document B arrives second and joins behind A
Document C arrives third and joins behind B
The printer processes them in that exact order: A, then B, then C. Document C cannot jump ahead no matter how small it is or how urgently you want it.
This ordering matters because it provides fairness. Every item eventually gets processed, and the wait time is proportional to how many items arrived before it. This property makes queues essential for any system where order of arrival determines order of service.
Contrast with stacks
Stacks follow Last In, First Out (LIFO). If you placed the same three documents on a stack, document C (the last one added) would come out first. This is useful when you need to reverse order or track nested state, but it violates fairness when items are waiting to be served.
Think of the difference this way:
Stack: A stack of plates. You add and remove from the top only.
Queue: A line at a ticket counter. You join at the back and leave from the front.
When you need fairness and ordering by arrival time, use a queue. When you need to reverse order or process the most recent item first, use a stack.
Queue operations
A queue supports a small set of core operations. Understanding these operations and their expected behavior is fundamental to using queues correctly.
Enqueue (add to back)
Enqueue adds an element to the back of the queue. This is also called "push," "add," or "offer" in various implementations.
After enqueueing, the new element becomes the last one that will be processed. All elements already in the queue are still ahead of it.
Dequeue (remove from front)
Dequeue removes and returns the element at the front of the queue. This is also called "pop," "remove," or "poll" in various implementations.
After dequeueing, the next element in line becomes the new front. The removed element is no longer part of the queue.
Peek (view front without removing)
Peek returns the front element without removing it. This is also called "front," "head," or "element" in various implementations.
Peek lets you inspect what will be dequeued next without changing the queue's state. This is useful when you need to make decisions based on the next item before committing to process it.
isEmpty (check if empty)
isEmpty returns true if the queue contains no elements, false otherwise. Some implementations provide a size or length property instead, where you check if the size equals zero.
Checking emptiness before dequeuing prevents errors. Attempting to dequeue from an empty queue is an error condition that different implementations handle differently (throwing an exception, returning null, or returning undefined).
Size (count elements)
Size returns the number of elements currently in the queue. This operation should be constant time, meaning the queue tracks its count rather than recounting on every call.
Array-based implementation
The simplest way to implement a queue uses an array. You track where the front and back are, and the array holds the elements in between.
Naive approach and its problem
The most straightforward approach uses an array where index 0 is always the front:
This works correctly but has a performance problem. The shift() operation removes the first element and shifts all remaining elements one position to the left. For a queue with n elements, dequeue takes O(n) time.
If you enqueue and dequeue 1,000,000 items, each dequeue shifts up to 1,000,000 elements. This adds up quickly.
Using an index pointer
A better approach uses a front index pointer instead of shifting:
Now dequeue is O(1) because you just increment the front pointer instead of shifting elements. The trade-off is that dequeued elements remain in the array, wasting memory.
Memory cleanup
The indexed approach leaks memory because dequeued elements stay in the array. You can periodically clean up:
The cleanup happens when half the array slots are wasted. This keeps memory usage bounded while making dequeue O(1) amortized (the occasional compaction costs O(n), but it happens infrequently enough that the average cost per operation is constant).
Time complexity summary for array implementation
Operation | Naive (shift) | Indexed | With cleanup |
|---|---|---|---|
enqueue | O(1) amortized | O(1) amortized | O(1) amortized |
dequeue | O(n) | O(1) | O(1) amortized |
peek | O(1) | O(1) | O(1) |
isEmpty | O(1) | O(1) | O(1) |
size | O(1) | O(1) | O(1) |
Linked list implementation
A linked list naturally supports queue operations with O(1) time for all operations without any cleanup logic.
Singly linked list queue
With a singly linked list, you maintain pointers to both the head (front) and tail (back):
How it works
Enqueue adds to the tail:
Create a new node
If the queue is empty, both head and tail point to the new node
Otherwise, link the current tail to the new node and update tail
Dequeue removes from the head:
Save the head's value
Move head to the next node
If the queue is now empty, also clear tail
Return the saved value
Both operations update a constant number of pointers regardless of queue size.
Time complexity for linked list implementation
Operation | Time complexity |
|---|---|
enqueue | O(1) |
dequeue | O(1) |
peek | O(1) |
isEmpty | O(1) |
size | O(1) |
Trade-offs between array and linked list
Array advantages:
Better memory locality (elements are contiguous in memory)
Less memory overhead per element (no pointers)
Faster in practice for small queues due to cache efficiency
Linked list advantages:
True O(1) operations without amortization
No wasted space from dequeued elements
No need for periodic cleanup or resizing logic
Memory usage always proportional to current size
When to use each:
Use arrays for small, performance-critical queues where cache efficiency matters
Use linked lists when you need guaranteed O(1) operations or when queue size varies dramatically
In languages with efficient built-in arrays (like JavaScript), the indexed array approach often performs well enough
Circular queue
A circular queue (also called a ring buffer) solves the memory waste problem of indexed arrays without needing cleanup. It treats the array as circular, wrapping around from the end back to the beginning.
The concept
Imagine the array as a circle rather than a line. When you reach the end, you wrap around to the beginning. This lets you reuse slots that were freed by dequeue operations.
Fixed-size implementation
A circular queue requires a fixed capacity because the circular indexing depends on knowing where to wrap:
How wrapping works
The key insight is the modulo operation: (index + 1) % capacity
If
backis at position 4 in a capacity-5 queue,(4 + 1) % 5 = 0, wrapping back to the startIf
backis at position 2,(2 + 1) % 5 = 3, normal increment
This arithmetic handles all cases uniformly without special conditionals for the end of the array.
Distinguishing full from empty
A naive circular queue using only front and back pointers cannot distinguish full from empty (both have front === back). There are three common solutions:
Track length separately (shown above): Add a length counter. Simple and clear.
Waste one slot: Never let the queue completely fill. Full means
(back + 1) % capacity === front. This wastes one slot but avoids tracking length.Use a flag: Add a boolean that tracks whether the last operation was enqueue or dequeue. If
front === backand last operation was enqueue, the queue is full; if dequeue, it is empty.
The length-tracking approach is recommended for clarity.
Dynamic resizing
You can make a circular queue resizable, though it requires copying elements:
Applications of circular queues
Circular queues excel when you have a bounded buffer:
Audio/video streaming: Buffer incoming data, consume at playback rate
Keyboard buffers: Store keystrokes until the system processes them
Network packet buffers: Hold incoming packets until the application reads them
Producer-consumer patterns: Fixed-size buffer between producer and consumer threads
Sliding window algorithms: Keep the last N items for moving averages or rate limiting
The fixed capacity is often a feature, not a limitation. It prevents unbounded memory growth and provides backpressure when producers outpace consumers.
Deque (double-ended queue)
A deque (pronounced "deck") supports insertion and deletion at both ends. It generalizes both stacks and queues: you can use it as a stack (add/remove from one end), a queue (add to one end, remove from the other), or something in between.
Operations
A deque supports six core operations:
Operation | Description |
|---|---|
addFront | Insert at the front |
addBack | Insert at the back |
removeFront | Remove from the front |
removeBack | Remove from the back |
peekFront | View front without removing |
peekBack | View back without removing |
Array-based implementation
This simple implementation has O(n) operations at the front due to shifting. For better performance, use a circular buffer or linked list.
Doubly linked list implementation
A doubly linked list provides O(1) operations at both ends:
Time complexity for deque implementations
Operation | Array (naive) | Doubly linked list |
|---|---|---|
addFront | O(n) | O(1) |
addBack | O(1) amortized | O(1) |
removeFront | O(n) | O(1) |
removeBack | O(1) | O(1) |
peekFront | O(1) | O(1) |
peekBack | O(1) | O(1) |
Applications of deques
Deques are useful when you need flexibility about which end to use:
Sliding window maximum/minimum: Keep a deque of useful elements, adding at back and removing from either end as the window slides
Work-stealing algorithms: Threads add to their own deque's back and steal from other deques' fronts
Undo/redo with limited history: Remove oldest entries from one end when the history limit is reached
Palindrome checking: Add characters to deque, then compare front and back as you remove from both ends
Priority queue introduction
A priority queue is a queue where elements have priorities, and dequeue always returns the element with the highest (or lowest) priority, regardless of insertion order.
This is not a FIFO structure. If you enqueue items A, B, C and B has the highest priority, dequeue returns B first, not A. Priority queues are covered in depth in the Heaps page, but understanding them relative to standard queues is valuable here.
Comparison with standard queues
Aspect | Standard queue | Priority queue |
|---|---|---|
Ordering | Insertion order (FIFO) | Priority order |
Dequeue returns | Oldest element | Highest priority element |
Use case | Fairness, ordering | Urgency, importance |
Basic interface
Implementation approaches
Sorted array: Keep elements sorted by priority. Enqueue is O(n) to find the right position; dequeue is O(1).
Unsorted array: Append on enqueue in O(1); scan for highest priority on dequeue in O(n).
Heap (recommended): A binary heap provides O(log n) for both enqueue and dequeue. This is the standard implementation.
Common applications
Task scheduling: Process high-priority tasks before low-priority ones
Event simulation: Process events in time order (priority = event time)
Dijkstra's algorithm: Always expand the node with the smallest distance
Huffman coding: Build trees by repeatedly combining the two lowest-frequency nodes
A pathfinding:* Expand nodes with the lowest estimated total cost
Priority queues are essential for algorithms where you always need "the best" item next, not just "the oldest" item.
Time complexity summary
Here is a comprehensive summary of time complexities for all queue variants:
Standard queue
Implementation | enqueue | dequeue | peek | isEmpty |
|---|---|---|---|---|
Array (shift) | O(1)* | O(n) | O(1) | O(1) |
Array (indexed) | O(1)* | O(1) | O(1) | O(1) |
Linked list | O(1) | O(1) | O(1) | O(1) |
Circular (fixed) | O(1) | O(1) | O(1) | O(1) |
Circular (dynamic) | O(1)* | O(1)* | O(1) | O(1) |
*Amortized time; occasional operations take longer due to resizing.
Deque
Implementation | addFront | addBack | removeFront | removeBack |
|---|---|---|---|---|
Array (naive) | O(n) | O(1)* | O(n) | O(1) |
Doubly linked list | O(1) | O(1) | O(1) | O(1) |
Circular buffer | O(1)* | O(1)* | O(1)* | O(1)* |
Priority queue
Implementation | enqueue | dequeue | peek |
|---|---|---|---|
Sorted array | O(n) | O(1) | O(1) |
Unsorted array | O(1) | O(n) | O(n) |
Binary heap | O(log n) | O(log n) | O(1) |
Common applications
Queues appear throughout computer science and software engineering. Understanding these applications helps you recognize when a queue is the right choice.
Breadth-first search (BFS)
BFS explores a graph level by level, visiting all neighbors of the current node before moving to neighbors' neighbors. A queue ensures you process nodes in the order they were discovered.
BFS is used for:
Finding shortest paths in unweighted graphs
Level-order traversal of trees
Finding all nodes within a certain distance
Web crawlers (visit pages in discovery order)
Task scheduling
Operating systems use queues to manage processes waiting for CPU time. Each process joins the queue when it needs the CPU and waits its turn.
This pattern appears in:
Print spoolers
Message queues (RabbitMQ, Kafka)
Job schedulers (cron jobs, background workers)
Event loops (JavaScript runtime)
Buffering
Queues act as buffers between components that operate at different speeds. The producer adds items to the queue, and the consumer removes them when ready.
Buffering with queues appears in:
Network I/O (TCP receive buffers)
Audio/video streaming (playback buffers)
Keyboard input (type-ahead buffer)
Inter-process communication (pipes)
Rate limiting and throttling
Queues help implement rate limiting by controlling how fast requests are processed:
Tree level-order traversal
BFS on a tree gives you level-order traversal, visiting all nodes at depth 0, then depth 1, and so on:
Cache eviction (LRU)
A queue helps track access order for Least Recently Used (LRU) cache eviction:
Edge cases and common pitfalls
Working with queues involves several edge cases you should handle carefully.
Empty queue operations
Always check for empty before dequeue or peek:
Different libraries handle this differently:
Some throw exceptions
Some return null or undefined
Some return a special "empty" value
Know what your implementation does and handle it consistently.
Single element queue
A queue with one element has the same element at front and back. After dequeueing, both front and back become invalid:
Forgetting to update tail when the queue becomes empty is a common bug.
Circular queue boundary conditions
With circular queues, test these cases specifically:
Enqueue into a full queue
Dequeue from an empty queue
Fill the queue completely, then empty it completely, then fill again
Operations when front > back (wrapped around)
Memory leaks in array implementations
When using arrays with index tracking, dequeued elements remain in memory:
Concurrent access
If multiple threads or async operations access the same queue, you need synchronization:
In JavaScript, single-threaded execution avoids most of these issues, but be careful with async operations that interleave.
Maximum size constraints
For bounded queues, decide how to handle overflow:
Choose the behavior that matches your use case:
Reject: When losing new items is acceptable (rate limiting)
Remove oldest: When old items become stale (streaming data)
Throw: When overflow indicates a bug that should be fixed
Using JavaScript's built-in structures
JavaScript arrays work as queues out of the box, though with performance trade-offs.
Array as queue
This is simple but shift() is O(n).
Map as ordered queue
JavaScript's Map maintains insertion order, which you can exploit:
This has O(1) amortized complexity for both operations in most JavaScript engines.
When to implement your own
Implement a custom queue when:
You need guaranteed O(1) operations (use linked list)
You need a bounded circular buffer
You need a deque with O(1) at both ends
You need thread-safe operations (in Node.js with workers)
The built-in array performance is measurably insufficient
For most applications, the built-in array with push/shift is sufficient.
Summary
Queues are fundamental data structures that enforce First In, First Out ordering. They appear throughout computing in task scheduling, buffering, graph algorithms, and anywhere that arrival order determines processing order.
Key takeaways:
FIFO principle: Elements are processed in the order they arrived. This provides fairness and predictable ordering.
Core operations: enqueue (add to back), dequeue (remove from front), peek (view front), and isEmpty are the essential operations. All should be O(1) in a good implementation.
Implementation choices: Arrays with index tracking work well for most cases. Linked lists provide guaranteed O(1) operations. Circular queues (ring buffers) efficiently handle bounded buffers.
Deques extend queues to support operations at both ends, enabling use as either a stack or a queue.
Priority queues break FIFO ordering to serve the highest-priority element first. They are typically implemented with heaps.
BFS relies on queues to visit graph nodes level by level. Any algorithm requiring "process in discovery order" likely needs a queue.
Edge cases matter: Empty queue operations, single-element transitions, circular buffer wraparound, and memory cleanup all require careful handling.
Choose based on your needs: For small, simple cases, use built-in arrays. For performance-critical code or specific requirements, implement a custom queue with the right trade-offs.
Understanding queues prepares you for more advanced topics like priority queues, BFS-based algorithms, and concurrent message passing systems that use queue semantics.