Home/Learn/RabbitMQ/Spring Retry with RabbitMQ

Spring Retry with RabbitMQ

Intermediate
Spring AMQP

Configure a RetryInterceptorBuilder on the listener container to automatically retry failed messages with back-off before routing to a DLQ.

Overview

When a @RabbitListener method throws an exception, Spring AMQP can automatically retry the message with configurable back-off before either nacking it (requeue or dead-letter) or routing it to a Dead Letter Queue (DLQ). Spring Retry integration is configured on the listener container factory via RetryInterceptorBuilder. This provides local retries (within the same application process, fast) for transient errors (temporary DB unavailability, network blips). For persistent errors that need retry with long delays, use RabbitMQ's native Dead Letter Exchange + per-queue TTL for delayed redelivery.

Configuring automatic retry with back-off

Add spring-retry to the classpath and configure RetryInterceptorBuilder on the SimpleRabbitListenerContainerFactory. Fixed back-off retries at constant intervals; exponential back-off doubles the delay between attempts. Set maxAttempts appropriately — retrying a non-transient error 5 times wastes time before routing to DLQ.

Java — RetryInterceptorBuilder with exponential back-off and DLQ fallback
<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

// Listener container factory with retry + exponential back-off
@Bean
public SimpleRabbitListenerContainerFactory retryListenerContainerFactory(
        ConnectionFactory connectionFactory,
        MessageConverter messageConverter) {

    SimpleRabbitListenerContainerFactory factory =
        new SimpleRabbitListenerContainerFactory();
    factory.setConnectionFactory(connectionFactory);
    factory.setMessageConverter(messageConverter);
    factory.setAcknowledgeMode(AcknowledgeMode.MANUAL); // manual ack

    // Exponential back-off retry: 1s, 2s, 4s, 8s — then send to DLQ
    RetryInterceptorBuilder<?> retryBuilder = RetryInterceptorBuilder.stateless()
        .maxAttempts(4)
        .backOffOptions(1000, 2.0, 8000)  // initialInterval, multiplier, maxInterval
        .recoverer(new RejectAndDontRequeueRecoverer());
        // After 4 attempts: NACK with requeue=false → message goes to DLQ

    factory.setAdviceChain(retryBuilder.build());
    return factory;
}

// Listener — exceptions trigger retry automatically
@RabbitListener(queues = "order.processing",
                containerFactory = "retryListenerContainerFactory")
public void processOrder(OrderMessage order) {
    // If this throws, Spring Retry catches it and retries
    // After maxAttempts: RejectAndDontRequeueRecoverer sends to DLQ
    orderService.process(order);
}

MessageRecoverer — custom handling after exhausted retries

MessageRecoverer is called after all retry attempts are exhausted. Three built-in options: RejectAndDontRequeueRecoverer (nack + dead-letter — most common), RepublishMessageRecoverer (publish to a specific error exchange with stack trace headers), and ImmediateRequeueMessageRecoverer (requeue — dangerous, may cause infinite loop).

Java — RepublishMessageRecoverer for structured DLQ with error headers
// RepublishMessageRecoverer — publish to error exchange with full context
@Bean
public MessageRecoverer republishRecoverer(RabbitTemplate rabbitTemplate) {
    RepublishMessageRecoverer recoverer =
        new RepublishMessageRecoverer(rabbitTemplate, "order.errors", "order.failed");
    // Failed message published to order.errors exchange with routing key order.failed
    // Headers added automatically:
    //   x-exception-stacktrace — full stack trace
    //   x-exception-message — exception message
    //   x-original-exchange — where the message came from
    //   x-original-routing-key
    recoverer.setErrorRoutingKeyPrefix("error.");
    return recoverer;
}

@Bean
public SimpleRabbitListenerContainerFactory retryListenerContainerFactory(
        ConnectionFactory cf, MessageConverter converter,
        MessageRecoverer recoverer) {
    SimpleRabbitListenerContainerFactory factory =
        new SimpleRabbitListenerContainerFactory();
    factory.setConnectionFactory(cf);
    factory.setMessageConverter(converter);
    factory.setAdviceChain(
        RetryInterceptorBuilder.stateless()
            .maxAttempts(3)
            .backOffOptions(2000, 2.0, 10000)
            .recoverer(recoverer)  // use RepublishMessageRecoverer
            .build()
    );
    return factory;
}

DLQ-based delayed retry with TTL queues

Spring Retry provides fast in-process retries (seconds). For retry-after-minutes or retry-after-hours, use RabbitMQ's Dead Letter Exchange with per-queue TTL: the failed message goes to a "wait" queue with a TTL (e.g. 5 minutes), then dead-letters back to the original queue for reprocessing. This creates a retry loop without a separate scheduler.

Java — TTL-based delayed retry loop: processing queue ↔ wait queue
// DLQ-based delayed retry: processing.queue → (on failure) → wait.queue (5 min TTL)
//                          → (after TTL expires) → processing.queue again

@Configuration
public class DelayedRetryConfig {

    @Bean
    public DirectExchange ordersExchange() {
        return new DirectExchange("orders");
    }

    @Bean
    public DirectExchange ordersWaitExchange() {
        return new DirectExchange("orders.wait");
    }

    // Main processing queue — messages dead-letter to wait exchange on rejection
    @Bean
    public Queue processingQueue() {
        return QueueBuilder.durable("order.processing")
            .withArgument("x-dead-letter-exchange", "orders.wait")
            .withArgument("x-dead-letter-routing-key", "order.wait")
            .build();
    }

    // Wait queue — messages expire after 5 min and return to processing queue
    @Bean
    public Queue waitQueue() {
        return QueueBuilder.durable("order.wait")
            .withArgument("x-message-ttl", 300_000)          // 5 minutes
            .withArgument("x-dead-letter-exchange", "orders") // back to main exchange
            .withArgument("x-dead-letter-routing-key", "order.process")
            .build();
    }

    // Bindings
    @Bean public Binding processingBinding() {
        return BindingBuilder.bind(processingQueue()).to(ordersExchange()).with("order.process");
    }
    @Bean public Binding waitBinding() {
        return BindingBuilder.bind(waitQueue()).to(ordersWaitExchange()).with("order.wait");
    }
}

Key Points to Remember

  • 1Spring Retry interceptor on the listener container provides in-process retries before routing to DLQ
  • 2RejectAndDontRequeueRecoverer nacks the message after exhausted retries → broker routes to Dead Letter Queue
  • 3RepublishMessageRecoverer publishes to a specific error exchange with stack trace headers — useful for structured DLQ analysis
  • 4Exponential back-off (1s, 2s, 4s) prevents hammering a struggling downstream service with rapid retries
  • 5In-process retry is for transient errors (seconds); DLQ + TTL queue loop is for longer retry delays (minutes/hours)
  • 6ImmediateRequeueMessageRecoverer requeues immediately — dangerous, can cause infinite hot loops on non-transient errors

Interview Questions

Sign in to ask Aria
1

What is the difference between Spring Retry and RabbitMQ's native Dead Letter Queue retry?

MediumThoughtworks
2

What does RejectAndDontRequeueRecoverer do after retry attempts are exhausted?

EasyInfosys
3

How would you implement a retry-after-5-minutes pattern using only RabbitMQ primitives?

HardNetflix
4

Why is ImmediateRequeueMessageRecoverer dangerous for non-transient errors?

MediumAmazon
5

What headers does RepublishMessageRecoverer add to dead-lettered messages?

MediumBooking.com

Ask Aria about Spring Retry with RabbitMQ

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…