Register your interest: Tag @Cody, get an agent
BlogEngineering

Message queues explained: what they buy you and what they cost

A queue decouples two systems in time, which solves real problems and introduces new ones. Delivery guarantees, ordering, back pressure, and the failures a queue makes possible.

Aymeric ZhuoAymeric Zhuo11 min read

Summarize with AI

Message queues explained: what they buy you and what they cost
On this page

A message queue lets one system hand work to another without waiting for it to finish. That is the whole idea, and the consequences are larger than it sounds: the two systems no longer have to be available at the same time, no longer have to run at the same speed, and no longer fail together.

What the description leaves out is that a queue converts one category of problem into another. Synchronous systems fail visibly and immediately; queued systems fail quietly and later, which is harder to notice and harder to reason about.

What we'll cover

What a queue actually does

Three things, and they are worth separating because people conflate them.

Decoupling in time. The producer writes and continues; the consumer reads whenever it can. Neither needs the other to be up at the same moment.

Buffering. A burst of work is absorbed rather than rejected, so a spike in incoming requests becomes a temporarily longer queue instead of a series of timeouts.

Rate matching. A fast producer and a slow consumer coexist, with the queue holding the difference.

What it does not do is make work disappear. The work still has to be done, and if the consumer is permanently slower than the producer, a queue delays the failure rather than preventing it — which is a useful thing to be clear about before adopting one.

The delivery guarantee you get

The single most consequential property, and the one most often misunderstood.

At-most-once means a message may be lost and will never be duplicated. Rarely what anybody wants.

At-least-once means a message will arrive and may arrive more than once. This is what nearly every practical system offers, and it is the guarantee you should design for.

Exactly-once is what people believe they have. In the general case, across a network and a separate system, it is not achievable — what is achievable is at-least-once delivery combined with idempotent processing, which produces the same outcome and is a different thing.

The consequence is unavoidable: your consumer must tolerate duplicates. Not as an edge case, but as normal behaviour, because retries, redeliveries after a crash, and visibility timeouts expiring mid-processing all produce them.

Acknowledge after processing, not before. Acknowledging on receipt means a crash during processing loses the message permanently, which is the most common way a queue-based system quietly drops work.

Ordering, which is weaker than people assume

Global ordering is rare and expensive. Most queues guarantee ordering within a partition or a group, not across the whole queue.

Concurrent consumers break ordering by definition. Two consumers processing in parallel will finish out of order regardless of what order they received work in.

Retries reorder. A message that fails and is retried arrives after messages that came later and succeeded.

Design so order does not matter where you can. Messages carrying the full state rather than a delta, and processing that is commutative, both remove the dependency.

Where order genuinely matters, use a partition key so related messages go to the same partition, and accept that this limits parallelism for that key.

Back pressure, and what happens when you ignore it

The thing that turns a queue from a shock absorber into a liability.

A queue that grows faster than it drains is a system already failing, just not visibly yet.

Unbounded queues fail worst. They accept work until memory, disk, or the provider's limit runs out, at which point everything stops at once rather than degrading.

Bounded queues force a decision: reject the producer, block it, or drop something. All three are unpleasant and all three are better than discovering the problem when the queue has a week of backlog.

Measure depth and age, not just throughput. Queue depth tells you the size of the backlog; the age of the oldest message tells you how far behind you are, which is what actually matters to whoever is waiting.

Alert on age. A queue holding a thousand messages that drains in a second is healthy; one holding fifty messages that arrived four hours ago is not, and only the age distinguishes them.

Failures a queue makes possible

Worth naming, because these are the costs on the other side of the ledger.

Silent backlog. Nothing errors; work is simply happening later, and later gets longer until somebody notices a downstream effect.

Poison messages. A message that always fails is retried forever, blocking or repeatedly consuming capacity — which is what a dead letter queue exists to catch.

Duplicate side effects. An email sent twice, an order created twice, a payment attempted twice. Every one of these is a direct consequence of at-least-once delivery meeting a non-idempotent consumer.

Lost context. Debugging becomes harder because the producer's stack trace and the consumer's failure are separated in time and place, so tracing has to be built deliberately.

Schema drift. The producer changes the message shape and the consumer, deployed separately, does not know — which is a versioning problem a synchronous call would have surfaced immediately.

When you do not need one

The honest section, because queues get adopted for their architecture-diagram value.

When the caller needs the answer, a queue adds latency and complexity to get you back to where a function call already was.

When volume is modest and steady, the buffering and rate-matching benefits do not apply.

When the operation is fast and reliable, and a timeout would be a genuine error rather than a spike to absorb.

When you have no way to observe the queue. An unmonitored queue is a place work goes to be forgotten.

When a database table would do. A table with a status column, polled, is a perfectly respectable queue for modest volumes and is far easier to inspect when something goes wrong.

Queue, log, or stream

The distinction matters when choosing, and the vocabulary is used loosely.

A classic queue delivers each message to one consumer and removes it. Good for work distribution, where each item should be done once.

A log keeps messages and lets consumers read at their own position, so several independent consumers each see everything. Good for event distribution and for replay.

Replay is the deciding feature. If you will ever need to reprocess history — a new consumer, a bug fix, a rebuilt projection — you want a log, and retrofitting one after the messages are gone is not possible.

Retention is finite either way, so "we can always replay" is true only within the window you configured.

Making the queue observable

A queue is a place work waits, and the whole failure mode is nobody knowing it is waiting.

Trace across the boundary. Carry a correlation identifier on the message so a producer's request and a consumer's failure can be connected, because they are separated in both time and process.

Log the message identifier on both sides, so a specific item can be followed rather than inferred.

Count what you reject. Messages that failed validation, arrived malformed, or were dropped as duplicates are three different numbers and a single "errors" count hides all of them.

Watch consumer lag as a rate. Whether the gap is growing or shrinking matters more than its size at any moment.

Make the dead letter queue visible to a person, with an owner. A dead letter queue nobody looks at is a folder where failures accumulate silently, which is worse than no dead letter queue because it creates the impression of handling.

Record the time from produced to processed, end to end. It is the number anybody downstream actually experiences, and it is usually not measured anywhere.

Using one with CodeWords

Most automation work does not need you to operate a queue yourself. CodeWords connects to more than 3,000 integrations, and the queueing behaviour — retries, ordering, and handling the message that keeps failing — is part of how an automation is built rather than infrastructure you maintain.

  1. Open CodeWords and start a new automation.
  2. Describe what should happen in plain language to Cody, the automation builder: what produces the work, what processes it, and what should happen if processing fails.
  3. Describe the exceptions explicitly: a duplicate, an item that fails repeatedly, a backlog that is growing.
  4. Connect the systems on either side, once, and reuse the connection.

You describe the outcome; Cody builds it, connects it, and deploys it. The free plan covers light use, with Pro at $39 per month and Business at $100 per month as usage grows; details are on the pricing page.

What to get right first

Idempotent processing. Before ordering, before partitioning, before anything about throughput — make it safe for the same message to be processed twice, because at-least-once delivery means it will be.

Two habits make the difference. Key on something from the message itself, not on a generated identifier, so two deliveries of the same message resolve to the same key. And alert on the age of the oldest message rather than queue depth, because depth without age cannot distinguish a busy healthy system from one that is quietly four hours behind.

Frequently asked questions

Do I get exactly-once delivery?

Not in the general case. What is achievable is at-least-once delivery plus idempotent processing, which produces the same result. Systems advertising exactly-once are usually offering this combination within their own boundary.

Why did the same thing happen twice?

At-least-once delivery met a consumer that is not idempotent. Retries, redelivery after a crash, and a visibility timeout expiring mid-processing all cause it, and none is an error condition.

When should I acknowledge a message?

After processing succeeds. Acknowledging on receipt means a crash during processing loses the message permanently, which is the most common way queued work disappears without anything reporting it.

Is ordering guaranteed?

Within a partition or group, usually. Globally, rarely — and concurrent consumers break it regardless. Design so order does not matter where you can, and use a partition key where it genuinely does.

What should I alert on?

The age of the oldest message, not queue depth. A deep queue that drains instantly is fine; a shallow one holding messages from four hours ago is not, and depth alone cannot tell you which you have.

Queue or log?

A queue when each item should be processed once by one consumer. A log when several consumers each need everything, or when you might ever want to replay history — which cannot be retrofitted after the messages have gone.

Is a database table a legitimate queue?

For modest volumes, yes, and it has a real advantage: you can inspect it with a query when something goes wrong. Reach for dedicated infrastructure when volume, retention, or multiple consumers make the table approach awkward.

What is the most commonly missed metric?

Time from produced to processed, end to end. Throughput and depth are both measured routinely; the number that describes what somebody downstream actually experiences usually is not measured at all.

Is a dead letter queue enough on its own?

No. A dead letter queue nobody monitors is a place failures accumulate silently, which is worse than not having one because it creates the impression the problem is handled. It needs an owner and an alert.

Get started today

Your first workflow is free to build.

Describe what you need. Cody handles the build, the connections, and the deployment.