In the world of enterprise AI, LLMs are inherently non-deterministic and flaky. Vector databases experience network partitions, LLM providers enforce strict rate limits, and users inevitably upload corrupted files. If your Multi-Agent LangGraph system relies on synchronous, in- memory execution, a single failure crashes the entire pipeline. In an event-driven architecture, a failure without proper handling results in infinite retry loops, draining your AWS bill and corrupting your state. To build a truly enterprise-grade AI system, you must master the triad of resilience: Retries, Dead Letter Queues (DLQs), and Poison-Message Handling.
In this article, we will explore how to implement these patterns end-to-end in an Enterprise Legal Contract Analysis RAG system using LangGraph and AWS SQS.
Part 1: The Theory of AI Resilience
Before writing code, we must distinguish between the three types of failures in an AI pipeline:
1. Transient Failures (The "Retry" Pattern)
What it is: Temporary issues. LLM API returns a
429 Too Many Requests, the Vector DB drops a connection, or a brief network blip occurs.The Solution: Exponential Backoff with Jitter. You retry the operation, waiting progressively longer each time, with a random jitter to prevent thundering herd problems.
Note: In LLM systems, we often handle these in-code (using libraries like
tenacity) rather than relying on SQS retries, because SQS visibility timeouts (minimum 0s, default 30s) are too slow for a 429 rate limit that clears in 2 seconds.
2. Systemic/Unknown Failures (The "DLQ" Pattern)
What it is: The system is fundamentally broken right now. The LLM provider is completely down, or the worker ran out of memory.
The Solution: Dead Letter Queues (DLQ). If a message fails processing
Ntimes (Max Receive Count), SQS automatically moves it to a DLQ. This stops the infinite loop, allowing engineers to investigate and replay the message later.
3. Poison Messages (The "Fail Fast" Pattern)
What it is: A message that will never succeed, no matter how many times you retry it. In AI, this happens when a user uploads a corrupted PDF, the payload has malformed JSON, or an LLM hallucinates a tool call with an invalid schema that crashes your parser.
The Solution: Poison-Message Handling. You must validate the payload before invoking the LangGraph. If it's poison, reject it immediately and route it to the DLQ. Do not waste 5 SQS retries on a poisoned message.
Part 2: The Use Case - Legal Contract RAG
The Scenario: A corporate legal team uses an AI system to analyze incoming M&A contracts.
The pipeline consists of three LangGraph agents:
Parser Agent: Extracts text and metadata from the uploaded contract.
Clause RAG Agent: Retrieves similar clauses from the firm's historical Vector DB and identifies risk factors.
Redlining Agent: Generates suggested legal edits.
The Failure Scenarios:
Transient: The Vector DB times out during the Clause RAG phase.
Poison: The user uploads a password-protected, encrypted PDF. The Parser Agent will never be able to read it.
Systemic: OpenAI experiences a global outage.

Part 3: End-to-End Code Implementation
Prerequisites: boto3, langgraph, langchain, tenacity (for robust retries)
Step 1: Provisioning SQS and the DLQ
First, we create the Main Queue and the Dead Letter Queue, linking them via a Redrive Policy.
import boto3
import json
import time
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
from langgraph.graph import StateGraph, END
from langchain_core.messages import HumanMessage, AIMessage
from typing import TypedDict, Annotated, Sequence
import operator
sqs = boto3.client('sqs')
# 1. Create the Dead Letter Queue (DLQ)
dlq = sqs.create_queue(QueueName='legal-contract-dlq')
dlq_arn = sqs.get_queue_attributes(
QueueUrl=dlq['QueueUrl'], AttributeNames=['QueueArn'])['Attributes']['QueueArn']
# 2. Create the Main Queue with a Redrive Policy
# MaxReceiveCount=3 means after 3 failed processing attempts, it goes to the DLQ
main_queue = sqs.create_queue(
QueueName='legal-contract-main',
Attributes={
'RedrivePolicy': json.dumps({
'deadLetterTargetArn': dlq_arn,
'maxReceiveCount': '3'
}),
'VisibilityTimeout': '60' # Give agents 60 seconds to process before returning to queue
}
)
main_queue_url = main_queue['QueueUrl']
print("Infrastructure ready.")Step 2: Defining the Multi-Agent LangGraph
We define our agents. Notice we use the @retry decorator from tenacity on the RAG/LLM calls to handle transient failures in-code.
# Custom Exception for Transient Errors
class TransientAIError(Exception): pass
# Custom Exception for Poison/Validation Errors
class PoisonMessageError(Exception): pass
# 1. Define State
class ContractState(TypedDict):
messages: Annotated[Sequence[HumanMessage | AIMessage], operator.add]
contract_id: str
file_s3_uri: str
extracted_text: str
risk_clauses: list
# 2. Define Nodes
def parser_agent(state: ContractState):
"""Extracts text. Fails fatally if file is encrypted (Poison)."""
print(f"[Parser] Reading {state['file_s3_uri']}...")
# Simulate checking for an encrypted/corrupted file
if "encrypted" in state['file_s3_uri']:
raise PoisonMessageError("File is encrypted and cannot be parsed.")
return {"extracted_text": "M&A Agreement between Corp A and Corp B...",
"messages": [AIMessage(content="Parsed.")]}
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=10),
retry=retry_if_exception_type(TransientAIError),
reraise=True
)
def clause_rag_agent(state: ContractState):
"""Queries Vector DB. Simulates transient network failures."""
print("[RAG Agent] Querying Vector DB for historical clauses...")
# Simulate a transient failure (e.g., Vector DB timeout)
if not hasattr(clause_rag_agent, "retry_count"):
clause_rag_agent.retry_count = 0
clause_rag_agent.retry_count += 1
if clause_rag_agent.retry_count < 2:
print(" -> Transient Error: Vector DB Connection Timeout!")
raise TransientAIError("Vector DB Timeout")
print(" -> Vector DB connection successful.")
return {"risk_clauses": ["Indemnification", "Limitation of Liability"],
"messages": [AIMessage(content="RAG complete.")]}
def redlining_agent(state: ContractState):
"""Generates edits."""
print("[Redlining Agent] Generating suggested edits via LLM...")
return {"messages": [AIMessage(content="Redlines generated.")]}
# 3. Build Graph
workflow = StateGraph(ContractState)
workflow.add_node("parse", parser_agent)
workflow.add_node("rag", clause_rag_agent)
workflow.add_node("redline", redlining_agent)
workflow.set_entry_point("parse")
workflow.add_edge("parse", "rag")
workflow.add_edge("rag", "redline")
workflow.add_edge("redline", END)
app = workflow.compile()
Join the conversation! Your thoughts help the community grow.