Dead Letter Channels in Apache Camel: Error Handling That Survives Production

Apache Camel's default error handler does something that surprises most teams the first time they see it in production: it does no redelivery at all. An exception propagates back to the consumer, the exchange fails, and what happens next depends entirely on the endpoint — a JMS consumer may redeliver forever, a file consumer may move on, an HTTP consumer returns a 500 and the message is simply gone. If your routes matter, error handling is not a finishing touch. It is the design.

Start with the dead letter channel

The Dead Letter Channel is one of the original enterprise integration patterns, and Camel's deadLetterChannel error handler is a faithful implementation: after redelivery is exhausted, the message is moved to a designated endpoint and the exchange is marked handled, so the consumer's own retry machinery stops.

errorHandler(deadLetterChannel("jms:queue:orders.DLQ")
    .maximumRedeliveries(5)
    .redeliveryDelay(1000)
    .backOffMultiplier(2)
    .useExponentialBackOff());

Three decisions are hiding in those four lines, and each deserves thought.

How many redeliveries, and how fast? Immediate retries only help with truly transient failures — a dropped connection, a lock timeout. If the downstream system is down for two minutes, five retries 50 ms apart accomplish nothing except filling your logs. Exponential backoff with a sensible cap is almost always right. For long outages, prefer fewer in-process redeliveries and let the message land in the DLQ, where a scheduled or manual replay can pick it up once the dependency is healthy.

Which message goes to the DLQ? By default Camel sends the exchange as it is at the point of failure — half-transformed, enriched, possibly stripped of the fields you need to reprocess it. useOriginalMessage() sends the message as it arrived at the start of the route instead. For any route that mutates the body, that is usually what you want: the DLQ becomes a queue of replayable inputs, not a museum of partial states.

Where does it go? A real queue on your broker, named consistently (<queue>.DLQ is a fine convention), with the same durability guarantees as the source queue. Writing dead letters to a log file is not a dead letter channel; it is a shredder with extra steps.

Not every exception deserves a retry

Redelivery policy is not one-size-fits-all. A validation failure will fail identically on attempt six; a connection reset probably will not. Camel's onException blocks let you split the two:

onException(ValidationException.class)
    .handled(true)
    .maximumRedeliveries(0)
    .to("jms:queue:orders.invalid");

onException(ConnectException.class)
    .maximumRedeliveries(8)
    .useExponentialBackOff();

Sorting exceptions into retryable and not retryable is one of the highest-value hours you can spend on a route. Retrying a poison message five times before parking it wastes time; retrying it forever takes the route down. And because redelivery re-runs the failing step, any step before the failure point has already executed — which is exactly why idempotency matters (a topic that deserves its own post).

A DLQ nobody watches is a black hole

The most common dead-letter failure we see in reviews is organizational, not technical: the queue exists, messages land in it, and nobody notices for three weeks. If a message was worth guaranteeing, it is worth alerting on. At minimum:

  • Alert on depth. Any nonzero depth on a DLQ that is normally empty should page someone during business hours.
  • Log with correlation. When Camel exhausts redeliveries, log the exception, the route id, and a business identifier — an order number beats an exchange id when a human has to answer "which customer did this affect?"
  • Have a replay story. Decide before the incident how messages get back into flow: a replay route reading from the DLQ, gated by a manual trigger, is thirty minutes of work on a calm day and unavailable at 2 a.m. during one.

The shape of a well-handled route

Put together, the pattern we build toward in most Camel estates looks like this: a dead letter channel with exponential backoff as the default; useOriginalMessage wherever the route transforms its input; onException carve-outs that stop retrying the unretryable; DLQs on the broker with depth alerts; and a documented replay path. None of it is exotic. All of it is the difference between an integration layer you trust and one you check nervously every morning.

Error handling is where integration systems earn their keep. The happy path is easy — any framework can move a message when everything is up. What you are really buying with Camel is a mature vocabulary for the unhappy path. Use it.