Dynamic Message Delays in RabbitMQ Without Plugins
By Ercan - 01/11/2025
Delayed messaging is a common requirement in distributed systems: scheduling retries, deferring notifications, or throttling workloads. RabbitMQ provides built-in support for message expiration and dead-lettering, but most tutorials rely on queue-level TTL. This approach has a major limitation: changing the TTL requires creating a new queue with updated arguments.
In real-world systems, however, the required delay may vary over time. Sometimes you need 5 seconds, sometimes 30 seconds, sometimes longer. Re-creating queues at runtime is not practical, and relying on external plugins (like rabbitmq_delayed_message_exchange) may not be an option.
This post demonstrates how to implement dynamic per-message delays in RabbitMQ using only native features—no plugins, no runtime queue creation.
TL;DR: If you want to skip the explanation and dive straight into the code, check out the implementation on GitHub:
https://github.com/ercansormaz/rabbitmq-dynamic-ttl
Problem Statement
- Queue-level TTL is static. Once defined, it applies to all messages in that queue.
- Adjusting the TTL requires creating a new queue with updated arguments.
- This is not flexible for runtime scenarios where the required delay may vary.
Our solution:
- Use per-message TTL (
expirationproperty) to define delays dynamically. - Route expired messages via a dead-letter routing into the final processing queue.
- Achieve runtime flexibility without plugins or new queues.
⚠️ Note on scalability: While it is technically possible to assign a TTL for each individual message, under heavy load RabbitMQ’s FIFO behavior can cause longer-TTL messages to block shorter ones. For large-scale systems, a bucketed delay strategy (e.g., 5s, 30s, 60s) is often more predictable.
Flows
Simple flow: simple-exchange → simple-queue → consumer
Delayed flow: delayed-exchange → delayed-wait-queue (per-message TTL) → delayed-queue → consumer
The delayed-wait-queue is configured with a dead-letter routing key pointing to delayed-queue. When a message’s TTL expires, RabbitMQ automatically forwards it to the final consumer queue.
Implementation Highlights
- Spring Boot application with REST endpoints to trigger both flows.
- Docker Compose setup to start RabbitMQ and the application together.
- RabbitMQ Management UI available at http://localhost:15672 (admin/s3cret).
Example Requests
Simple Flow
curl --location 'http://localhost:8080/message/simple' \
--header 'Content-Type: application/json' \
--data '{
"message": "Test Message",
"delayMs": 5000
}'
Delayed Flow
curl --location 'http://localhost:8080/message/delayed' \
--header 'Content-Type: application/json' \
--data '{
"message": "Test Message",
"delayMs": 5000
}'
Sample Logs
Delayed consumer:
[DELAYED_QUEUE_CONSUMER] [PUBLISH_DATE=Sat Nov 01 12:55:26 UTC 2025] [MESSAGE=Test Message] [DELAY=5000]
Simple consumer:
[SIMPLE_QUEUE_CONSUMER] [PUBLISH_DATE=Sat Nov 01 12:55:26 UTC 2025] [MESSAGE=Test Message]
Measuring the Actual Delay
Each log entry contains both the publish date and the log timestamp. By comparing them, you can verify the effective delay.
Example: If the delayed consumer log is printed at 12:55:31, then:
12:55:31 (log timestamp) - 12:55:26 (publish date) = 5 seconds
This matches the requested delay (5000 ms).
Key Takeaways
- Dynamic per-message TTL enables flexible delayed messaging without plugins.
- FIFO ordering can cause longer delays to block shorter ones—design accordingly.
- For high-load systems, consider delay buckets instead of arbitrary per-message TTLs.
- This approach is lightweight, portable, and works with vanilla RabbitMQ.
👉 You can explore the source code on GitHub:
https://github.com/ercansormaz/rabbitmq-dynamic-ttl
Tags: spring boot, rabbitmq
