Skip to content
Knowledge Hub

RabbitMQ ack, prefetch, publisher confirms: three different boundaries Intermediate

Estimated reading time: 7 min read

A producer sends two invoice-creation jobs. The log says “sent”, but after a restart no invoice exists. On the consumer side, the log shows “received job-1” but nothing was written to the database; the connection closed and the job showed up again.

These two observations do not contradict each other. RabbitMQ has three independent boundaries, and bugs usually come from lumping them together under one word, “ack”.

Boundary Who tells whom Confirms Does not confirm
Publisher confirm Broker → producer The broker has taken responsibility for the message (quorum queue: after replicating to a majority) That the message was routed correctly, or that a consumer processed it
Consumer ack Consumer → broker The delivery is done; the broker need not redeliver The DB does not roll back if you ack too early
Prefetch Consumer configures it with the broker Limits the number of unacked deliveries Confirms nothing at all
Confirm closes the publisher leg, ack closes the consumer leg, prefetch throttles in between
Confirm closes the publisher leg, ack closes the consumer leg, prefetch throttles in betweenStep 1/7: The publisher sends job-1 on a confirm channel. Nothing is guaranteed yet.timepublish job-1Publisherconfirm channelQuorum queuebrokerConsumerprefetch=1, manual ack
Step 1/7

The publisher sends job-1 on a confirm channel. Nothing is guaranteed yet.

The key point from the diagram: between “confirm received” and “basic.ack” lies the whole time the consumer spends writing the database. The two milestones can be seconds or hours apart, and nothing ties them together.

Confirms run on a confirm channel. For persistent messages into a durable queue, the broker confirms only after writing to disk; for quorum queues, the confirm is sent only after the message is replicated to a majority of replicas.

Three things often missed:

  • Unroutable messages are still confirmed. If no queue matches, the broker confirms once the exchange decides not to route. To find out, publish with mandatory to receive basic.return (which arrives before basic.ack), or use an alternate exchange.
  • A basic.nack from the broker is rare (an internal error in the queue process). When it happens, the publisher should republish.
  • Do not wait for a confirm after every message. The docs recommend handling confirms asynchronously or publishing in batches, with a cap on outstanding unconfirmed publishes.

noAck: true is auto ack: the broker considers the message done as soon as it is sent. Only suitable for data you can afford to lose. For important jobs, use manual ack and ack after the durable side effect.

Crash point Result
Before the DB commit The job is redelivered, no work lost
After the DB commit, before the ack The job is redelivered (redelivered = true), dedupe needed
Ack before the DB commit Work lost: the broker has no reason to redeliver

Two protocol errors to avoid: acking the same delivery tag twice, or acking on a different channel than the one that received the delivery. Both close the channel with PRECONDITION_FAILED - unknown delivery tag.

nack or reject with requeue=true puts the message straight back in line, so permanent errors loop hot. The fix lives in dead-lettering and counted retries. Preventing duplicate processing lives in idempotent consumer. The redelivered flag is only a hint (see that article), not a substitute for a message ID.

consumer_timeout: acking too late is also an error

Section titled “consumer_timeout: acking too late is also an error”

RabbitMQ sets a deadline for an unacked delivery. The default is 30 minutes. Past it, the channel is closed with PRECONDITION_FAILED, and every unacked delivery on that channel (from all consumers) is requeued.

  • Checked roughly every minute; values below 1 minute are unsupported, and below 5 minutes are not recommended.
  • Can be set per queue (policy consumer-timeout or argument x-consumer-timeout, since 3.12).
  • Since 4.3 this timeout applies only to quorum queues, with different behavior per the quorum queues docs: the expired consumer’s messages return to the queue; an AMQP 0-9-1 consumer is cancelled if the client supports consumer_cancel_notify, otherwise the channel is closed with PRECONDITION_FAILED.
  • The “channel closed” description above is pre-4.3 behavior (the Consumers page still says so); check against the exact version you run.
  • Jobs longer than the timeout need a design change (split them, ack early with durable state), not just a bigger number.
# rabbitmq.conf: unit is milliseconds (example: 1 hour)
consumer_timeout = 3600000

Prefetch is the maximum number of unacked deliveries the broker pushes ahead to each consumer (basic.qos applies separately to each new consumer on the channel when global=false). prefetch=0 means unlimited.

Quorum queues and streams do not support global QoS: consuming on a channel with global set causes a channel error. Use per-consumer prefetch.

Situation Suggested prefetch Reason
Long, heavy jobs that should be spread evenly across workers 1 A worker only gets the next job when free, and does not “hoard” work
Short jobs, low processing latency, throughput needed Increase gradually (tens to a few hundred), then measure Avoid the broker waiting for an ack per message
A consumer processing N jobs in parallel About N or more Keep workers from idling while waiting for deliveries

The “suggested” column is operational experience, not numbers from the official docs. The docs only say that prefetch limits in-flight volume, and that if consumer capacity is below 100%, raising prefetch may help the queue deliver faster. Measure consumer capacity, memory and fairness before settling.

Terminal window
docker compose -f examples/docker-compose.yml up -d rabbitmq
npx --prefix examples ts-node examples/src/rabbitmq-ack/ack-prefetch-demo.ts

Output on a local broker (temporary queue, prefetch(1), the consumer waits 200 ms before acking): publisher: broker confirmed two messages, consumer received: job-1, consumer ack: job-1, consumer received: job-2. It shows that job-2 arrives only after job-1 is acked.

The temporary queue is exclusive, autoDelete and non-durable, so the demo does not verify persistence or redelivery across restarts.

Try removing consumer.ack(message) for the first job: job-2 never reaches this consumer because the window is still full.

  • Manual ack + confirms: jobs that must not be lost (invoices, payments, transactional email).
  • Auto ack: telemetry, cache invalidation, data whose loss is harmless.
  • Prefetch = 1: heavy jobs with widely varying processing time.
  • Large prefetch: light, uniform jobs where throughput comes first.
  1. Publishers use a confirm channel; confirms are handled asynchronously with a cap on unconfirmed publishes.
  2. Important events: mandatory or an alternate exchange, and monitor basic.return.
  3. Consumers use manual ack, acking after the durable side effect.
  4. Handlers are idempotent by message/event ID.
  5. Set prefetch per workload; do not leave slow consumers at the unlimited default.
  6. Know consumer_timeout (30 minutes), and make sure your longest job stays under it.
  7. Do not use global QoS with quorum queues and streams.
  8. Separate dashboards for unconfirmed, ready, unacked, redelivery.

Self-check: (1) The confirm has arrived; has the invoice been written? (2) Why does prefetch=1 stop job-2 from reaching the same consumer? (3) What happens to a 45-minute job on a default queue?