skip to content

Explain nack-requeue vs reject-without-requeue in a Spring AMQP listener, and how the wrong choice creates a poison-message loop.

level: middleimportance: must knowfreq 75%

answer

  1. defaultRequeueRejected default = true
  2. requeue=true -> redelivered near head -> loop
  3. AmqpRejectAndDontRequeueException -> requeue=false
  4. poison = deterministic failure
  5. reject-no-requeue -> DLX or drop

basics

~20 s

Nack with requeue=true puts the message back on the queue to be redelivered; reject (requeue=false) discards or dead-letters it. If a message always fails and you keep requeuing it, it's redelivered forever — a poison-message loop. Reject-without-requeue breaks the loop.

solid answer

~40 s

When a listener throws, the container acks/nacks based on defaultRequeueRejected (default true), so by default the message is requeued and redelivered. If the failure is deterministic (bad payload, validation error), that message will fail again immediately and loop forever, blocking the queue — a poison message. To break it, the listener should reject *without* requeue: throw AmqpRejectAndDontRequeueException (or set defaultRequeueRejected=false), which nacks with requeue=false, dropping the message or dead-lettering it to a DLX if configured. The rule of thumb: requeue only for *transient* failures (network blip, temporary lock) where redelivery might succeed; reject-without-requeue for *permanent* failures. Blindly requeuing on every exception is the classic poison-loop bug.

code

java · 22 lines
java
@RabbitListener(queues = "orders")
public void handle(OrderMessage msg) {
    try {
        orderService.process(msg);
    } catch (TransientDataAccessException ex) {
        // transient: let the container requeue (redelivery may succeed)
        throw new AmqpException("retry later", ex);
    } catch (ValidationException ex) {
        // permanent/poison: do NOT requeue -> dead-letter it
        throw new AmqpRejectAndDontRequeueException("invalid order, dead-lettering", ex);
    }
}

// Or globally reject-without-requeue on any exception:
@Bean
SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
        ConnectionFactory cf, SimpleRabbitListenerContainerFactoryConfigurer cfg) {
    var factory = new SimpleRabbitListenerContainerFactory();
    cfg.configure(factory, cf);
    factory.setDefaultRequeueRejected(false); // any throw -> requeue=false
    return factory;
}

go deeper

for a junior

Know requeue=true means redeliver and can loop; reject-without-requeue stops it.

for a middle

Know defaultRequeueRejected defaults to true and AmqpRejectAndDontRequeueException forces requeue=false.

for a senior

Distinguish transient vs permanent failures and route each correctly; pair reject with a DLX.

for a principal

Define a poison-message policy (max redeliveries, FatalExceptionStrategy, DLQ alerting) across the platform.

**The core distinction:** When a Spring AMQP `@RabbitListener` finishes processing, the container tells the broker one of three things via the AMQP protocol: - **ack** (`basic.ack`) — success, remove the message. - **nack/reject with requeue=true** — failed, but *put it back on the queue* to be redelivered (to this or another consumer). - **nack/reject with requeue=false** — failed permanently, *don't put it back*: the broker drops it, or dead-letters it if the queue has a DLX. **Default behavior (critical to know):** The container property `defaultRequeueRejected` defaults to **true**. So when your listener method throws an exception, the default outcome is **requeue=true** — the message goes back on the queue. **Why that creates a poison-message loop:** A *poison message* is one that **deterministically fails every time** — a malformed payload, a schema violation, a business-rule breach. With `requeue=true`, the broker immediately redelivers it, the listener throws again, it's requeued again... an infinite tight loop that pins CPU, floods logs, and can head-of-line-block the queue for good messages. Because RabbitMQ puts a requeued message back near the **head**, the same poison message is often the very next one you get. **How to break the loop — reject without requeue:** Two mechanisms: 1. Throw **`AmqpRejectAndDontRequeueException`** from the listener (or let a `MessageRecoverer`/`RejectAndDontRequeueRecoverer` throw it). Regardless of `defaultRequeueRejected`, this forces requeue=false. 2. Set the container factory's **`defaultRequeueRejected=false`** so *any* thrown exception rejects without requeue. Either way, requeue=false means: if the queue has an `x-dead-letter-exchange`, the message is dead-lettered to the DLQ; otherwise it's discarded. **The reverse gotcha — `ImmediateAcknowledgeAmqpException`:** Throwing this acks the message as if successful (no requeue, no dead-letter) — useful to *silently drop* without a DLQ. **Nuances:** - Spring's built-in retry (RetryInterceptor) does its retries *in memory* by default (stateless), and only after retries are exhausted does the recoverer decide requeue vs reject. So 'requeue' at the broker level and 'retry' in the app are different layers. - With **manual acks** (`AcknowledgeMode.MANUAL`) you call `channel.basicNack(tag, false, requeue)` yourself and own the decision. - Deciding requeue for transient vs reject for permanent failures often means catching specific exception types: e.g. requeue on `TransientDataAccessException`, reject on `MessageConversionException` / validation errors. Spring lets you configure this via a `FatalExceptionStrategy` or by throwing the right exception type. **When to use which:** Requeue (requeue=true) only when a *retry could plausibly succeed* and you don't have app-level retry handling the transient case. Reject-without-requeue for anything deterministic, and always pair it with a DLX so you don't lose the evidence.

  • A colleague sets defaultRequeueRejected=false to stop a poison loop but there's no DLX. What's the risk?
    Reject-without-requeue with no dead-letter exchange means the message is silently discarded. You stop the loop but lose the message and any chance to inspect/replay it. Always pair reject-no-requeue with a DLX.
  • How do you make a message ack (drop silently) as if it succeeded, without dead-lettering?
    Throw ImmediateAcknowledgeAmqpException from the listener; the container acks the delivery, so it's neither requeued nor dead-lettered.

saying these in an interview costs you the question

  • Claiming requeue=true is a safe default for all failures
  • Thinking reject-without-requeue always sends to a DLQ even when no DLX is configured
  • Confusing app-level retry with broker-level requeue

context