Queues and Messages Deep Dive
Queue properties and types (classic, quorum, streams), overflow and lazy behavior, message anatomy, persistence, TTL, and priority.
Prerequisite:Exchanges, Bindings and Routing TopologiesYou’ll need: A broker running locally from Environment Setup.
Exchanges decide where a message goes. Queues decide how it is stored and for how long, and messages carry the data and metadata that later modules act on. This module is the single source of truth for queue types, message anatomy, and TTL.
What you’ll be able to do after this module
- Configure queue durability, exclusivity, and auto-delete correctly.
- Choose between classic, quorum, and stream queues.
- Control queue length with max-length and overflow behavior.
- Describe the parts of a message: payload, properties, and headers.
- Explain message persistence and how it interacts with durable queues.
- Apply TTL at the message and queue level, and set up priority queues.
1. Queue properties
Every queue is declared with a few key properties:
| Property | Meaning | Typical value |
|---|---|---|
| Durable | The queue definition survives a broker restart. | true for anything that matters |
| Exclusive | Used by only one connection and deleted when that connection closes. | false except for private, temporary queues |
| Auto-delete | Deleted automatically once the last consumer unsubscribes. | false for shared work queues |
Caution: Durable is about the queue surviving a restart. It does not by itself make the messages inside survive. That also requires persistent messages, covered in section 5.
@Bean
Queue inventoryQueue() {
return QueueBuilder.durable("inventory.queue").build();
}
2. Queue types: classic, quorum, streams
Modern RabbitMQ offers three queue types with different durability and use cases.
flowchart TD
Start["Need to store messages"]
Q1{"Need replay of<br/>history / very high throughput?"}
Q2{"Need high availability<br/>across a cluster?"}
Streams["Stream queue"]
Quorum["Quorum queue"]
Classic["Classic queue"]
Start --> Q1
Q1 -->|yes| Streams
Q1 -->|no| Q2
Q2 -->|yes| Quorum
Q2 -->|no| Classic
| Type | What it is | Choose when |
|---|---|---|
| Classic | The original single-node queue. Simple and fast. | Local dev, non-critical work, or single-node brokers |
| Quorum | Replicated, Raft-based queue built for data safety across a cluster. The modern default for important work. | Production work that must survive node failures |
| Stream | An append-only log that supports replay and many readers of the same data at high throughput. | Event replay and streaming workloads (covered in the Streams module) |
@Bean
Queue ordersQuorum() {
return QueueBuilder.durable("orders.queue").quorum().build();
}
For production Spring Boot services in this course, prefer quorum queues for anything you cannot afford to lose.
3. Length limits and overflow
A queue can be capped so an unbounded producer does not fill the broker’s memory or disk. When the cap is reached, the overflow behavior decides what happens.
| Argument | Effect |
|---|---|
x-max-length | Maximum number of messages |
x-max-length-bytes | Maximum total size |
x-overflow: drop-head | Drop the oldest messages to make room (default) |
x-overflow: reject-publish | Reject new messages so producers get a signal |
@Bean
Queue boundedQueue() {
return QueueBuilder.durable("bounded.queue")
.maxLength(10_000)
.overflow(Overflow.rejectPublish)
.build();
}
Combine reject-publish with publisher confirms (covered in the Reliability section) so producers know when a queue is full instead of losing data silently.
Note: Classic queues historically had a separate “lazy” mode to keep messages on disk. Modern RabbitMQ (classic queue version 2 and quorum queues) manages memory this way by default, so you rarely set lazy mode explicitly anymore.
4. Message anatomy
A message is more than its payload. It travels as an envelope of three parts.
flowchart TD
subgraph msg [Message]
Props["Properties<br/>delivery mode, content-type,<br/>priority, expiration,<br/>correlation-id, reply-to,<br/>message-id, timestamp"]
Headers["Headers<br/>arbitrary key/value map<br/>(e.g. region, x-death)"]
Body["Body<br/>the payload bytes<br/>(often JSON)"]
end
- Properties are standard AMQP fields the broker and clients understand, such as
delivery_mode,content_type,priority,expiration,correlation_id, andreply_to. - Headers are an arbitrary key/value map you control, useful for routing on a headers exchange or carrying metadata like a tenant or region. The broker also adds some, such as
x-deathwhen a message is dead-lettered. - Body is the raw payload. In Spring AMQP a message converter turns your DTO into bytes and back, covered in the Building with Spring AMQP section.
In Spring AMQP you set these through MessageProperties:
MessageProperties props = MessagePropertiesBuilder.newInstance()
.setContentType("application/json")
.setHeader("region", "eu")
.setPriority(5)
.build();
Message message = new Message(payloadBytes, props);
5. Persistence and delivery mode
A message’s delivery mode decides whether it is written to disk:
- Persistent (
delivery_mode = 2): written to disk, so it can survive a broker restart. - Transient (
delivery_mode = 1): kept in memory only.
Durability requires both halves: a durable queue to survive the restart, and persistent messages so the contents survive with it. A persistent message in a non-durable queue, or a transient message in a durable queue, can still be lost.
In Spring AMQP, RabbitTemplate sends persistent messages by default. You can set it explicitly on MessageProperties with setDeliveryMode(MessageDeliveryMode.PERSISTENT).
Caution: Persistence protects against broker restarts, not against a message never being safely accepted in the first place. Confirming that the broker accepted a publish is the job of publisher confirms, in the Reliability section.
6. Time to live (TTL)
TTL controls how long a message can live before it is discarded or dead-lettered. It can be set two ways:
| Scope | How | Effect |
|---|---|---|
| Per queue | x-message-ttl queue argument | Every message in the queue expires after this many milliseconds |
| Per message | expiration message property | That single message expires after this many milliseconds |
When both apply, the shorter one wins. You can also expire an unused queue itself with x-expires.
@Bean
Queue ttlQueue() {
return QueueBuilder.durable("orders.pending.queue")
.ttl(60_000) // x-message-ttl: 60s
.deadLetterExchange("orders.dlx")
.build();
}
An expired message is not always lost. If the queue has a dead-letter exchange, expired messages are routed there instead. This pairing is the basis of delayed-retry patterns, covered in the Dead Letter Exchanges module.
7. Priority queues
A queue can be declared with a maximum priority so higher-priority messages are delivered ahead of lower-priority ones.
@Bean
Queue priorityQueue() {
return QueueBuilder.durable("orders.priority.queue")
.maxPriority(10)
.build();
}
Publishers set a message’s priority property (0 to the queue maximum). Use priorities sparingly: they help when a small fraction of traffic is urgent, but they do not replace proper capacity planning, and they add overhead.
Checkpoint
You should now be able to:
- Set queue durability, exclusivity, and auto-delete appropriately.
- Choose classic, quorum, or stream queues for a workload.
- Cap a queue and pick an overflow behavior.
- Name the three parts of a message envelope.
- Explain why durability needs both a durable queue and persistent messages.
- Apply per-queue and per-message TTL.