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 (expiration property) 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