Backend Development 8 min read

Handling Dead Letter Queues in RabbitMQ with Spring Boot

This article explains what dead letters are in RabbitMQ, outlines their common causes, and provides a complete Spring Boot configuration and code examples for setting up dead‑letter exchanges, queues, TTL handling, message rejection, and consumer processing to reliably manage undeliverable messages.

IT Architects Alliance
IT Architects Alliance
IT Architects Alliance
Handling Dead Letter Queues in RabbitMQ with Spring Boot

Dead letters are messages that cannot be consumed and are routed to a special dead‑letter queue (DLX) via a dead‑letter‑exchange.

Sources of dead letters

Message TTL expiration

Queue reaching its maximum length

Message rejection using basic.reject or basic.nack with requeue = false

To handle dead letters, the article sets up normal and dead‑letter exchanges and queues using Spring Boot configuration.

@Configuration
public class DeadConfig {
    // Normal exchange
    @Bean
    DirectExchange normalExchange() {
        return new DirectExchange("normalExchange", true, false);
    }
    // Normal queue (max length 5) with dead‑letter arguments
    @Bean
    Queue normalQueue() {
        Map
args = deadQueueArgs();
        args.put("x-max-length", 5);
        return new Queue("normalQueue", true, false, false, args);
    }
    // TTL queue (message TTL 60 seconds) without consumers
    @Bean
    Queue ttlQueue() {
        Map
args = deadQueueArgs();
        args.put("x-message-ttl", 60 * 1000);
        return new Queue("ttlQueue", true, false, false, args);
    }
    // Bindings for normal and TTL queues
    @Bean
    Binding normalRouteBinding() {
        return BindingBuilder.bind(normalQueue()).to(normalExchange()).with("normalRouting");
    }
    @Bean
    Binding ttlRouteBinding() {
        return BindingBuilder.bind(ttlQueue()).to(normalExchange()).with("ttlRouting");
    }
    // Dead‑letter exchange and queue
    @Bean
    DirectExchange deadExchange() {
        return new DirectExchange("deadExchange", true, false);
    }
    @Bean
    Queue deadQueue() {
        return new Queue("deadQueue", true, false, false);
    }
    @Bean
    Binding deadRouteBinding() {
        return BindingBuilder.bind(deadQueue()).to(deadExchange()).with("deadRouting");
    }
    // Arguments that bind a queue to the dead‑letter exchange
    private Map
deadQueueArgs() {
        Map
map = new HashMap<>();
        map.put("x-dead-letter-exchange", "deadExchange");
        map.put("x-dead-letter-routing-key", "deadRouting");
        return map;
    }
}

Sending messages to the normal queue (max length 5) is demonstrated with a REST endpoint that creates a map containing a UUID and timestamp, then publishes it using rabbitTemplate.convertAndSend .

@GetMapping("/normalQueue")
public String normalQueue() {
    Map
map = new HashMap<>();
    map.put("messageId", UUID.randomUUID().toString());
    map.put("data", System.currentTimeMillis() + ", normal queue message, max length 5");
    rabbitTemplate.convertAndSend("normalExchange", "normalRouting", map, new CorrelationData());
    return JSONObject.toJSONString(map);
}

A second endpoint shows how to send a message that will expire after 60 seconds, causing it to be dead‑lettered.

@GetMapping("/ttlToDead")
public String ttlToDead() {
    Map
map = new HashMap<>();
    map.put("messageId", UUID.randomUUID().toString());
    map.put("data", System.currentTimeMillis() + ", ttl queue message");
    rabbitTemplate.convertAndSend("normalExchange", "ttlRouting", map, new CorrelationData());
    return JSONObject.toJSONString(map);
}

The consumer for the normal queue deliberately rejects messages without re‑queueing, triggering dead‑letter routing.

@Component
@RabbitListener(queues = "normalQueue")
public class NormalConsumer {
    @RabbitHandler
    public void process(Map message, Channel channel, Message mqMsg) throws IOException {
        System.out.println("Received and reject without requeue: " + message);
        channel.basicReject(mqMsg.getMessageProperties().getDeliveryTag(), false);
    }
}

The dead‑letter queue consumer acknowledges messages, confirming they have been successfully routed.

@Component
@RabbitListener(queues = "deadQueue")
public class DeadConsumer {
    @RabbitHandler
    public void process(Map message, Channel channel, Message mqMsg) throws IOException {
        System.out.println("Dead queue received message: " + message);
        channel.basicAck(mqMsg.getMessageProperties().getDeliveryTag(), false);
    }
}

Running the demo shows messages from the normal queue being dead‑lettered when the queue exceeds its length, when TTL expires, or when the consumer rejects them, with the dead‑letter queue receiving and logging each message.

backendSpring BootRabbitMQMessagingDead Letter QueueMessage RejectionMessage TTL
IT Architects Alliance
Written by

IT Architects Alliance

Discussion and exchange on system, internet, large‑scale distributed, high‑availability, and high‑performance architectures, as well as big data, machine learning, AI, and architecture adjustments with internet technologies. Includes real‑world large‑scale architecture case studies. Open to architects who have ideas and enjoy sharing.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.