Software Engineering

Queues

September 8, 2026 · 6 min read · Angelos Ioakeimidis

I wrote about stacks last time, and queues are the natural next stop - same idea of “you can only touch one end of the list”, just the opposite rule about which end. Once stacks clicked for me, queues took about five minutes, because most of what I already knew just flips.

A stack flips the rule to LIFO - last in, first out - and shows up in undo buttons and the call stack. Read Stack →

Here’s my take on what a queue is, the two moves you need, and a few places you’ve probably used one without noticing.

What a queue even is

A queue is a list where new items go in at the back, and items come out from the front. You can’t cut the line and you can’t pull someone out of the middle.

The technical term is FIFO - First In, First Out. Whatever got added first is the first thing that leaves.

Think of an actual queue at a shop. You join at the back. Whoever’s been standing there longest gets served first. Nobody’s letting you skip to the front just because you showed up in a hurry.

The two operations you actually need

Same shape as a stack, just different names and different ends:

  • enqueue - add something to the back
  • dequeue - remove whatever’s at the front and hand it back to you
Animated diagram showing how enqueue and dequeue works

Some implementations add a peek too, so you can check what’s next without removing it yet.

Here’s a queue in plain JavaScript. I’m using an object with a front and back pointer instead of a plain array, because shifting an array with .shift() is O(n) - it has to renumber every remaining item. This version keeps both operations O(1):

Queue Implementation
class Queue {
  #items = {}
  #front = 0
  #back = 0

  enqueue(item) {
    this.#items[this.#back] = item
    this.#back++
  }

  dequeue() {
    if (this.isEmpty()) return undefined
    const item = this.#items[this.#front]

    delete this.#item[this.#front]
    this.#front++
    return item
  }

  peek() {
    return this.#items[this.#front]
  }

  isEmpty() {
    return this.#front === this.#back
  }

  get size() {
    return this.#back - this.#front
  }
}

The #front and #back counters just keep track of which index is next up and which index is next free, so I never have to reshuffle the whole object every time something leaves.

Where you’ve already used one: task runners

This is the example that made queues click for me. Say you’re building something that has to process jobs one at a time - resizing uploaded images, sending emails, whatever. You don’t want two jobs stepping on each other, so you queue them up and work through them in order.

Basic Job Queue
const jobQueue = new Queue()
let isProcessing = false

function addJob(job) {
  jobQueue.enqueue(job)
  processNext()
}

async function processNext() {
  if (isProcessing || jobQueue.isEmpty()) return

  isProcessing = true
  const job = jobQueue.dequeue()

  await job()

  isProcessing = false
  processNext()
}

Every job that comes in gets enqueued, and processNext just keeps working through the front of the line until it’s empty. Whoever submitted first gets handled first - nobody’s job jumps the queue because it happened to be smaller or arrived from a faster connection.

Your printer is doing the same thing

This one’s almost too literal, but it’s the clearest real-world FIFO there is. Send three documents to a shared office printer, and they come out in the order you sent them, not the order you’d prefer. Document one goes in first, prints first, and leaves the queue. Document two moves up to the front and does the same.

If you’ve ever watched a print job get “stuck” and hold up everything behind it, that’s the FIFO rule at work too - nothing behind it can jump ahead, it just has to wait its turn.

The one that’s running right now, without you asking for it

JavaScript’s own event loop leans on a queue too. When a setTimeout fires, or a promise resolves, or an event handler is ready to run, it doesn’t execute immediately - it gets placed into a queue (the task queue, or for promises specifically, the microtask queue), and the event loop works through that queue in order once the current code finishes running.

The Task Queue in Action
console.log('first')

setTimeout(() => console.log('third'), 0)

Promise.resolve().then(() => console.log('second'))

console.log('fourth, but logs before the timeout')

That logs first, then fourth, but logs before the timeout, then second, then third - even though the setTimeout was written before the promise. Microtasks (promises) get their own queue that drains before the regular task queue gets touched, but within each queue, it’s still FIFO: whatever got queued first runs first.

Here’s a little demonstration to visualise it:

Animated diagram showing how the task queue works

I’ve got a whole separate post on this if you want the full breakdown of tasks vs microtasks.

If you’ve ever written a breadth-first search over a tree or graph, you’ve used a queue whether you called it that or not. BFS visits nodes level by level - everything one step away, then everything two steps away, and so on.

Breadth-First Search
function bfs(startNode) {
  const visited = new Set([startNode])
  const queue = new Queue()
  queue.enqueue(startNode)

  while (!queue.isEmpty()) {
    const node = queue.dequeue()
    console.log(node.value)

    for (const neighbor of node.neighbors) {
      if (!visited.has(neighbor)) {
        visited.add(neighbor)
        queue.enqueue(neighbor)
      }
    }
  }
}

Every neighbor gets enqueued as it’s discovered, so the nodes closest to the start always get dequeued and processed before anything further away. Swap that queue for a stack and you’d get depth-first search instead - same code, completely different traversal order, just because of which end you’re pulling from.

Stack vs Queue, quickly

Same comparison from the stack post, just from the other side. A stack is LIFO, last in first out - reach for it when you want to undo the most recent thing or unwind something in reverse order. A queue is FIFO, first in first out - reach for it when order of arrival actually matters, like a task runner, a print queue, or level-by-level traversal.

Missed the stack post?

The call stack is a stack in disguise - LIFO order, pushing a frame on every call and popping one on every return.

Read Stack →
Key Takeaways
  • A queue only allows two moves: enqueue (add to the back) and dequeue (remove from the front) - FIFO, first in first out.

  • Job runners and task processors use a queue so work gets handled in the order it arrived.

  • Print queues are a real-world FIFO: the first document sent is the first one printed.

  • Breadth-first search uses a queue to visit nodes level by level, closest first.

  • Stack vs queue: a stack reverses order (undo, recursion), a queue preserves arrival order (task runners, BFS).

Liked this article? Share it with a friend on Twitter or support me to take on more ambitious projects to write about. Have a question, feedback or simply wish to contact me privately? Shoot me a DM and I'll do my best to get back to you.

Have a wonderful day.

Angelos Ioakeimidis

Angelos Ioakeimidis

Data Science & AI Student. Writing about software engineering, design, and AI.