Queues and circular buffers
First in, first out — and why `shift()` on an array is the wrong way to build one.
A queue adds at the back and removes from the front: enqueue, dequeue, peek. It is the structure behind job processing, request handling, breadth-first search, and every printer that has ever kept you waiting.
The trap
const queue = [];
queue.push(job); // O(1) — fine
queue.shift(); // O(n) — every remaining element moves left
Draining a million-job array with shift() is O(n²). It works in a demo and dies in production.
Two ends, two pointers
Keep a head index instead of moving the data:
class Queue {
#items = [];
#head = 0;
enqueue(value) { this.#items.push(value); }
dequeue() {
if (this.#head >= this.#items.length) return undefined;
const value = this.#items[this.#head++];
// Reclaim space once the dead prefix outweighs the live part.
if (this.#head > 32 && this.#head * 2 >= this.#items.length) {
this.#items = this.#items.slice(this.#head);
this.#head = 0;
}
return value;
}
get size() { return this.#items.length - this.#head; }
}
O(1) amortised at both ends. A linked list with head and tail pointers gives the same guarantee without the compaction, at the cost of a node per item.
Circular buffers
When capacity is fixed, wrap around a preallocated array with modular arithmetic. No allocation, no garbage, constant memory:
class RingBuffer {
constructor(capacity) {
this.items = new Array(capacity);
this.capacity = capacity;
this.head = 0;
this.size = 0;
}
push(value) {
const at = (this.head + this.size) % this.capacity;
this.items[at] = value;
if (this.size === this.capacity) this.head = (this.head + 1) % this.capacity; // overwrite oldest
else this.size++;
}
toArray() {
return Array.from({ length: this.size }, (_, i) => this.items[(this.head + i) % this.capacity]);
}
}
This is how audio buffers, log tailers, metric windows and network stacks hold "the last n things" without ever allocating.
Queues you will meet by name
- Priority queue — served by importance, not arrival. That is a heap, three chapters on.
- Deque — both ends open, the basis of the sliding window maximum.
- Blocking queue — the producer–consumer backbone of concurrent systems.
- Message queue — the same contract across a network, with durability added.