Practical guide
How to design a reliable message consumer
Acknowledge a message only after completing the work and expect it to return after a crash.
In short
At-least-once means possible duplicates
A broker may redeliver when a consumer commits a database change but crashes before acknowledgement. Reliable design does not promise one delivery; it guarantees one business effect.
RabbitMQ retains an unacknowledged delivery and exposes it to another consumer after channel loss. The application needs manual ack, idempotency, and a bounded failure path.
Prepare
What you need
Define one business effect and a stable identity used to recognize redelivery first.
- A versioned message schema with message ID, type, occurred_at, and business operation identifier.
- A database unique key or naturally idempotent operation protecting against duplication.
- Transient and permanent failure classes, a retry limit, and a dead-letter path.
- Metrics for processing, failures, retries, redelivery, handler duration, lag, and worker health.
Steps 1 to 3
Wrap processing in a safe unit
Validation, idempotent database effect, and acknowledgement have an exact order. Ack is not cleanup in a finally block.
1. Validate before doing work
- Validate content type, event type, schema version, required fields, and size before calling domain logic.
- Reject an unsupported version or permanently invalid data without requeue into the DLQ with a safe reason.
- Propagate correlation ID to logs and metrics but redact payload and credentials.
- Never trust a class or command name directly from a message. Map message types through an explicit handler allowlist.
message type + schema version → known handler Official RabbitMQ consumer documentation 2. Apply an idempotent effect, then ack
- In one database transaction, insert the message ID into a processed table and apply the business change. A unique constraint resolves a concurrent duplicate.
- If the message ID already exists, verify completed meaning and acknowledge without repeating the effect.
- Send ack only after a successful commit. An early acknowledgement can lose work permanently after a crash.
- If processing publishes another message, write it to an outbox in the same transaction. Ack and publish are not one atomic operation.
BEGIN → INSERT processed_message → business change → outbox → COMMIT → ACK Official acknowledgement documentation 3. Bound concurrency, retry, and shutdown
- Set prefetch according to work duration and memory. Unlimited unacknowledged deliveries hurt fair distribution and crash recovery.
- Retry transient failures with increasing delay and a limit. Send a permanent failure without requeue to the DLQ.
- On SIGTERM, stop accepting new messages, finish or safely interrupt active work, and ack only successful deliveries.
- Split very long jobs or set operational timeout deliberately. A worker that stops acknowledging must be visible in monitoring.
prefetch=20; manualAck=true; retry=3; gracefulShutdown=true Official prefetch documentation Step 4
Test crashes at the worst moment
The happy path does not verify reliability. Kill the process between commit and ack and inspect the business result.
-
Crash after commit before ack
The message is redelivered, but the unique idempotency record prevents a second change and second outbox event.
php bin/phpunit --filter ConsumerRedelivery -
Return transient and permanent failures
A transient error follows bounded retry and a permanent one goes directly to the DLQ. Neither creates a hot requeue loop.
-
Stop a worker under load
After SIGTERM it accepts no new messages. Acknowledged jobs are complete and the broker redelivers unacknowledged ones.
When it goes wrong
Common mistakes
An order was created twice
The consumer assumes one delivery. Add a stable business or message ID, a unique constraint, and a record in the same transaction as the change.
A message disappeared without completed work
Ack was sent before commit or automatically on receipt. Enable manual ack and confirm only a completed effect.
One worker holds too many messages
Lower prefetch and inspect handler duration. The value must match concurrency and memory, not maximum throughput in an empty test.
Redeliveries rise after deployment
Workers may stop without a drain phase or exceed acknowledgement timeout. Add graceful shutdown and measure handler duration.
Done
The consumer handles duplicates, failures, and restarts.
RabbitMQ can redeliver safely: idempotency protects the business effect, ack follows commit, and failures have a bounded path.