Hermes Wiki
Developer/SchedulingQueueingDisciplines/FIFO/Fundamentals/fifo-queueing-discipline-and-head-of-line-blocking

FIFO Queueing Discipline and Head-of-Line Blocking

Concept

First-in-first-out is the default ordering discipline for almost every queue in a distributed system — message brokers, load balancer backlogs, thread-pool work queues, network transmit buffers — because it's the discipline that matches most people's intuition of "fair": whoever arrived first gets served first. Enqueue and dequeue are both O(1), and the guarantee is simple to reason about: for any two items A and B, if A entered before B, A leaves before B.

That guarantee is also FIFO's central liability, known as head-of-line (HOL) blocking: because the item at the front of the queue must be fully processed (or explicitly skipped) before anything behind it can proceed, one slow or stuck item stalls every item queued after it, even items that are individually ready to go and have nothing to do with the stuck one. This shows up at every layer — a TCP segment lost in transit blocks delivery of every later segment to the application even though they already arrived (solved at the transport layer by QUIC's independent per-stream delivery); a single slow consumer in a naive single-threaded queue processor stalls the entire backlog behind it; a poison-pill message that a consumer can't process holds up every message queued after it until it's manually removed or dead-lettered.

Production FIFO systems solve HOL blocking not by abandoning ordering but by narrowing what "in order" actually means. Amazon SQS FIFO queues are the clearest production example: ordering is guaranteed only within a MessageGroupId, not across the whole queue. Messages in different groups can and do get delivered out of order relative to each other, and different groups can be processed concurrently by different consumers — but within one group, exactly one consumer processes messages one at a time, strictly in order. Choosing a granular, high-cardinality group ID (e.g., a per-order or per-listing ID rather than a single global group) is what turns a strictly serial FIFO queue into something that scales — the total ordering guarantee narrows to the only scope that usually matters (events about the same entity), which reopens parallelism everywhere else.

Tradeoffs

Ordering scope Throughput / parallelism HOL blocking blast radius Complexity
Global FIFO (single total order) Lowest — one consumer, strictly serial Worst — any stuck item stalls the entire queue Lowest to reason about
Grouped FIFO (order within a partition/group key, e.g. SQS MessageGroupId, Kafka partition key) High — groups process in parallel Contained to one group; other groups unaffected Moderate — requires choosing a good group/partition key
No ordering guarantee (standard/at-most-once queue) Highest — fully parallel, no serialization point None (nothing to block on) Lowest, but callers must tolerate out-of-order and possibly duplicate delivery
FIFO with explicit dead-lettering Same as its base tier, plus resilience to poison pills A stuck item is quarantined after N failed attempts instead of blocking forever Higher — needs a DLQ, alerting, and a redrive process

The real design lever isn't "FIFO or not" — it's choosing the partition key that ordering is scoped to. Too coarse (one global order) reintroduces full HOL blocking; too fine (a unique key per message) removes any real ordering guarantee at all, since nothing shares a group with anything else.

When to use / when not to

  • Use FIFO (globally or grouped) whenever downstream correctness depends on order — payment state transitions, inventory decrement-then-check sequences, event-sourced aggregates where replay order changes the result.
  • Prefer a grouped/partitioned FIFO discipline over a single global queue the moment throughput matters — a global total order caps you at one consumer's throughput no matter how much you scale out.
  • Choose the group/partition key at the same granularity as your consistency boundary (e.g., per-order, per-user, per-aggregate) — the same intuition as choosing a shard key for a partitioned database (see Consistent Hashing): you want writes to the same logical entity to always land in the same partition.
  • Don't reach for strict ordering when consumers are naturally idempotent and commutative (e.g., independent metric increments) — a standard, unordered queue avoids HOL blocking entirely and scales further.
  • Always pair FIFO with a dead-letter mechanism in production — without one, a single malformed or unprocessable message becomes a permanent outage for everything behind it in that queue/group.

Common pitfall

Picking a group/partition key that's too coarse "to be safe" — e.g., a single shared group ID for an entire tenant or the whole system — and then being surprised that throughput caps out at what one consumer can process, because SQS FIFO (and equivalent systems) guarantee only one in-flight message per group at a time. This is the message-queue analogue of a database hot partition: the ordering guarantee you actually need was scoped to "events about the same order/entity," but the key chosen was scoped to "everything," so every unrelated event now serializes behind every other one.

Engineering Lens

The useful design-review question isn't "is this queue FIFO" — it's "what's the actual entity whose events must stay ordered, and does the partition key match that entity exactly." Getting this scoping right is what separates a FIFO queue that scales from one that becomes a throughput ceiling under load; it's the same reasoning that governs shard-key selection in a partitioned database, applied to message ordering instead of storage. A queue that can't state precisely what it's serializing on is usually over-scoped for the ordering guarantee it actually needs.

Sources

Hermes Wiki