Problem: DecompositionFailedException in RAG Query Decomposition Service
The RAG decomposition microservice consumes user queries from the rag.requests exchange, splits them into sub‑queries, and republishes each sub‑query to topic queues such as rag.subqueries.*. When a complex query (e.g., nested JSON, multi‑entity request, or very long prompt) is processed, the service throws DecompositionFailedException. The exception propagates, the original RabbitMQ message is nacked without requeue, and downstream retrieval/generation services receive no sub‑queries.
Typical observable symptoms include:
- Log entry:
DecompositionFailedException: Failed to decompose query – token limit exceeded - RabbitMQ warning:
basic.nack received for delivery-tag 42, requeue=false - Dead‑letter queue accumulation (if DLX configured) or silent message loss (if no DLX)
- Missing retrieval jobs in monitoring dashboards, leading to increased latency for end‑users
Root Cause Analysis
Why the exception occurs
The decomposition chain (often built on LangChain or LlamaIndex) relies on an LLM call to split the query. Two failure modes dominate:
- Token‑limit overflow: Complex queries exceed the LLM’s maximum input tokens. The splitter raises
DecompositionFailedExceptionwith the message “token limit exceeded”.
Evidence: GitHub issue langchain-ai/langchain#8421 and facebookresearch/llama-index#1234 report identical behavior. - Timeout or runtime error in the LLM call: Under high load the LLM service does not respond within the configured 30 s window, causing a
TimeoutErrorthat the splitter converts intoDecompositionFailedException.
Evidence: Incident “SaaS analytics tool (Mar 2025)” shows a 406 PRECONDITION_FAILED error after the service timed out.
Both cases leave the consumer without a successful processing path. Because the service does not explicitly ack the original message nor nack with requeue=true, RabbitMQ treats the exception as a hard failure and drops the message (or routes it to a dead‑letter exchange if configured).
Interaction with RabbitMQ
According to the RabbitMQ official documentation on exchanges and routing, the decomposition service must republish sub‑queries to a valid exchange. When the service crashes before publishing, the channel may be closed, leading to errors such as:
Channel closed: 406 PRECONDITION_FAILED - unknown exchange 'rag.subqueries'
Furthermore, the publisher confirms mechanism is often disabled, so the service cannot detect that the sub‑query publish failed.
Investigation and Debugging
Log inspection
2025-05-12 14:03:27,842 ERROR rag.decomposer - DecompositionFailedException: Failed to decompose query – token limit exceeded
2025-05-12 14:03:27,845 WARN rabbitmq - basic.nack received for delivery-tag 57, requeue=false
2025-05-12 14:03:27,850 INFO rabbitmq - Consumer cancelled by server: consumer cancelled due to exception in callback
RabbitMQ metrics
- Check
queue.rag.requests.messages_unacknowledged– spikes indicate messages stuck in processing. - Inspect dead‑letter queue
rag.dlqfor nacked messages.
Reproduce locally
# Publish a complex query that triggers the failure
curl -X POST http://frontend.local/publish \
-d '{"query":"Find all orders where customer.address.city = \"San Francisco\" and items contain a nested JSON with specifications > 10"}' \
-H "Content-Type: application/json"
# Consume with the decomposition service in debug mode
java -jar rag-decomposer.jar --log-level=DEBUG
Observe the stack trace and confirm whether the exception originates from the LLM client or the splitter logic.
Check consumer QoS
High prefetch values can cause back‑pressure, leading to timeouts. Verify the prefetch setting:
channel.basicQos(5); // recommended for low‑latency RAG pipelines
Reference: RabbitMQ Consumer Prefetch and QoS documentation.
Resolution
Code changes
Introduce robust error handling around the decomposition step and ensure proper message acknowledgement.
// Before (simplified)
try {
List<SubQuery> parts = splitter.decompose(message.getBody());
for (SubQuery part : parts) {
channel.basicPublish("rag.subqueries", part.getTopic(), null, part.toBytes());
}
channel.basicAck(deliveryTag, false);
} catch (DecompositionFailedException e) {
logger.error("Decomposition failed", e);
// No ack/nack -> message lost
}
// After – explicit nack with requeue and fallback publishing
try {
List<SubQuery> parts = splitter.decompose(message.getBody());
for (SubQuery part : parts) {
channel.basicPublish("rag.subqueries", part.getTopic(), null, part.toBytes());
}
channel.basicAck(deliveryTag, false);
} catch (DecompositionFailedException e) {
logger.error("Decomposition failed: {}", e.getMessage());
// Publish a fallback “fallback” sub‑query or mark as failed
SubQuery fallback = SubQuery.fallback(message.getBody());
channel.basicPublish("rag.subqueries.fallback", "", null, fallback.toBytes());
// Nack with requeue=false so it goes to DLX (if configured)
channel.basicNack(deliveryTag, false, false);
}
LLM request adjustments
- Increase the LLM token limit or truncate input before splitting.
- Add a retry with exponential back‑off for timeout errors.
- Set a lower
maxTokensfor the splitter to stay within model limits.
RabbitMQ configuration
| Setting | Recommended Value | Rationale |
|---|---|---|
| Exchange for sub‑queries | rag.subqueries (topic) |
Ensure existence before deployment |
| Dead‑letter exchange | rag.dlx |
Capture failed messages for later analysis |
| Consumer prefetch | 5–10 | Prevent channel blockage under load |
| Publisher confirms | Enabled | Detect publish failures immediately |
Configure DLX as per RabbitMQ Dead Letter Exchanges documentation.
Verification
Functional test
# Publish a known complex query
curl -X POST http://frontend.local/publish -d '{"query":""}' -H "Content-Type: application/json"
# Expect sub‑queries in the topic queue
rabbitmqadmin get queue name=rag.subqueries.complex count=10 requeue=false
Successful output should list sub‑query messages with proper routing keys.
Metrics validation
- Confirm
queue.rag.requests.messages_unacknowledgeddrops to near zero. - Check dead‑letter queue depth – should only contain messages that truly cannot be split after fallback handling.
- Verify that the decomposition service’s
consumer_latency_msmetric stays below the SLA threshold (e.g., < 200 ms).
Prevention and Best Practices
- Input sanitization: Truncate or chunk queries exceeding the LLM’s token limit before invoking the splitter.
- Graceful degradation: Always publish a fallback sub‑query or error payload to avoid silent drops.
- Publisher confirms: Enable confirms on the channel to catch routing failures early.
- Dead‑letter handling: Configure a DLX for the
rag.subqueriesexchange; monitor its depth and set alerts on sudden spikes. - Consumer QoS: Tune prefetch to match processing capacity; combine with circuit‑breaker patterns for downstream LLM calls.
- Observability: Emit structured logs with correlation IDs (e.g.,
request_id) and expose Prometheus metrics for decomposition success/failure rates.
Related Topic Hub: Data Infrastructure Troubleshooting Hub
FAQ
- Why does the exception only appear for long queries?
Because the splitter forwards the entire query to the LLM. When the token count exceeds the model’s limit, the LLM returns an error that the splitter translates intoDecompositionFailedException. Truncating or chunking the input resolves the issue. - Can I rely on RabbitMQ’s automatic requeue after an exception?
No. By default, a consumer that throws an exception must explicitlybasicNackwithrequeue=true. Otherwise the message is dropped or dead‑lettered, as seen in the “basic.nack … requeue=false” log entries. - How do I know whether a sub‑query failed to publish?
Enable publisher confirms (channel.confirmSelect()) and listen forBasic.AckorBasic.Nackcallbacks. Log a warning if a confirm is not received within the timeout. - What is the proper way to configure a dead‑letter exchange for this pipeline?
Declare a DLX (e.g.,rag.dlx) and bind it to the original queue withx-dead-letter-exchange=rag.dlx. Then route dead‑lettered messages to a monitoring queue for manual inspection. - Is increasing the consumer prefetch value a good way to improve throughput?
Higher prefetch can increase throughput but also amplifies the impact of a single failure (more messages stay unacknowledged). For RAG pipelines, a modest prefetch (5–10) balances latency and reliability.