Implementing RabbitMQ Delayed Queues with Dead‑Letter Exchanges
This guide explains common use cases for delayed tasks, describes the two RabbitMQ delay‑queue methods (TTL + dead‑letter exchange and the rabbitmq‑delayed‑message‑exchange plugin), and provides step‑by‑step PHP code for configuring dead‑letter exchanges, queues, and timed message processing.
Typical Scenarios for Delayed Tasks
IoT systems need to timeout commands that receive no response.
E‑commerce orders are automatically cancelled if unpaid after 30 minutes.
Welcome emails are sent one minute after a new user registers.
Two Ways to Achieve Delayed Queues in RabbitMQ
Use message expiration (TTL) combined with a dead‑letter exchange to forward expired messages to a delay queue.
Install the rabbitmq-delayed-message-exchange plugin (available from RabbitMQ 3.5.7) to provide native delayed‑message support.
TTL (Time‑To‑Live) Mechanism
RabbitMQ allows setting a TTL for individual messages via the expiration field (in milliseconds) or for an entire queue via the x-message-ttl argument. When the TTL expires, the message becomes a dead letter.
$body = 'Tinywan expiration!';
$msg = new AMQPMessage($body);
$msg->set('delivery_mode', AMQPMessage::DELIVERY_MODE_PERSISTENT);
// Set timeout (ms)
$msg->set('expiration', 30000); // 30 secondsThe message will be discarded or routed to the dead‑letter queue only when it reaches the head of the queue and is evaluated for expiration.
Dead‑Letter Exchange (DLX)
A dead‑letter exchange receives messages that become dead letters due to rejection, TTL expiry, or queue length limits. By configuring x-dead-letter-exchange and optionally x-dead-letter-routing-key on a queue, RabbitMQ automatically forwards such messages to the specified exchange and its bound dead‑letter queue.
Typical dead‑letter conditions:
Consumer rejects a message with requeue = false.
Message TTL expires.
Queue reaches its maximum length.
Setting Up a Dead‑Letter Queue
1. Declare the dead‑letter exchange and queue, then bind them:
Exchange: dlx.exchange
Queue: dlx.queue
RoutingKey: #
# "#" routes all messages to the queue.2. Add a listener to process messages from the dead‑letter queue.
3. When declaring the primary queue, add the argument "x-dead-letter-exchange":"dlx.exchange":
arguments.put("x-dead-letter-exchange", "dlx.exchange");Now, messages that expire, are rejected, or exceed queue length will be routed to the dead‑letter queue.
Using Dead‑Letter Queues for Scheduled Tasks
By setting a message’s expiration and consuming from the dead‑letter queue bound to the original queue’s DLX, you can implement timed processing. For example, a message in queue1 with DLX deadEx1 will, upon expiry, be sent to deadQueue1 where a consumer can handle the delayed task.
Complete PHP Example
Producer (delayQueueSend)
public static function delayQueueSend($param = [])
{
$connection = RabbitMqConnection::getConnection();
$channel = $connection->channel();
// Declare exchanges
$channel->exchange_declare('waitSendExchange', 'fanout', false, false, false);
$channel->exchange_declare('expireExchange', 'fanout', false, false, false);
// Declare queues
$channel->queue_declare('expireQueue', false, false, false, false);
$channel->queue_declare('waitSendQueue', false, false, false, false, false,
new AMQPTable(["x-dead-letter-exchange" => "expireExchange"]));
// Bind queues
$channel->queue_bind('waitSendQueue', 'waitSendExchange');
$channel->queue_bind('expireQueue', 'expireExchange');
$body = 'Tinywan expiration!';
$msg = new AMQPMessage($body);
$msg->set('delivery_mode', AMQPMessage::DELIVERY_MODE_PERSISTENT);
$msg->set('expiration', 30000); // 30 s
// Publish to waiting exchange
$channel->basic_publish($msg, 'waitSendExchange');
echo "[x] Sent " . date('Y-m-d H:i:s') . ': ' . $body . "
";
$channel->close();
$connection->close();
}Consumer (delayQueueReceive)
public static function delayQueueReceive()
{
$connection = RabbitMqConnection::getConnection();
$channel = $connection->channel();
// Declare exchanges
$channel->exchange_declare('waitSendExchange', 'fanout', false, false, false);
$channel->exchange_declare('expireExchange', 'fanout', false, false, false);
// Declare queues
$channel->queue_declare('expireQueue', false, false, false, false);
$channel->queue_declare('waitSendQueue', false, false, false, false, false,
new AMQPTable(["x-dead-letter-exchange" => "expireExchange"]));
// Bind queues
$channel->queue_bind('waitSendQueue', 'waitSendExchange');
$channel->queue_bind('expireQueue', 'expireExchange');
echo "[*] Waiting for message. To exit press CTRL+C
";
$callback = function($msg) {
echo "[x] Receive " . date('Y-m-d H:i:s') . ': ' . $msg->body . "
";
};
$channel->basic_consume('expireQueue', "", false, true, false, false, $callback);
while (count($channel->callbacks)) {
$channel->wait();
}
$channel->close();
$connection->close();
}Important Notes
If expireExchange is declared as a direct exchange, you must provide a binding key (the dead‑letter routing key) when binding queues; otherwise, declare it as fanout to avoid specifying a binding key.
Execution Results
Sending side prints the sent message with a timestamp; receiving side prints the consumed message when the TTL expires and the message is routed to the dead‑letter queue.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Open Source Tech Hub
Sharing cutting-edge internet technologies and practical AI resources.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
