Priority queues in practice
A queue served by importance — and the tie-breaking and starvation problems that come with it.
A priority queue is the ADT; a heap is how you implement it. Instead of first-in-first-out, the highest-priority item is served next.
const queue = new MinHeap((a, b) => a.priority - b.priority);
queue.push({ priority: 3, job: 'send digest email' });
queue.push({ priority: 1, job: 'password reset' });
queue.push({ priority: 2, job: 'resize avatar' });
queue.pop(); // the password reset — priority 1 wins
Ties need a rule
Heaps are not stable: two items with equal priority come out in unspecified order. When arrival order matters, add a monotonically increasing counter as a tiebreaker:
let sequence = 0;
const queue = new MinHeap((a, b) => a.priority - b.priority || a.seq - b.seq);
const enqueue = (priority, job) => queue.push({ priority, seq: sequence++, job });
Without it, a job can sit behind newer jobs of the same priority indefinitely, and the bug is intermittent and very hard to reproduce.
Starvation
Strict priority means low-priority work may never run while high-priority work keeps arriving. Real schedulers add ageing: raise an item's priority the longer it waits.
const effective = (job, now) => job.priority - Math.floor((now - job.queuedAt) / 60_000);
This is not a data-structure problem — the heap is doing exactly what it was asked. It is a policy problem, and naming it in a system design interview scores well.
Where you will meet one
- Dijkstra and A* — always expand the nearest unvisited node.
- Task schedulers and event loops — the next timer to fire is a min-heap by timestamp.
- Huffman coding — repeatedly merge the two least frequent symbols.
- Merging k sorted streams — a heap of the current heads.
- Rate limiting and expiry — the soonest-to-expire entry.
Updating a priority
Changing an item's priority after insertion needs decreaseKey, which requires knowing where the item sits — a heap alone cannot find it. Two options: keep a map from item to index and update it on every swap, or push a new entry and skip stale ones on pop:
const [priority, node] = queue.pop();
if (priority > best[node]) continue; // stale entry, already improved
The lazy version is what most Dijkstra implementations do, because it is far simpler and costs only a little extra memory.