In enterprise AI, building a multi-agent LangGraph RAG system is only half the battle; keeping it alive during a traffic spike is the other. Multi-agent architectures are inherently "chatty." A single user query might trigger a supervisor agent, three specialized RAG retrieval agents, and an action agent, resulting in dozens of LLM calls and database queries.
When traffic spikes, AWS Lambda scales infinitely (up to your account limit), but your downstream dependencies—LLM API rate limits, Vector DB connection pools, and internal microservices—do not. This creates the "Thundering Herd" problem: a massive concurrency spike that overwhelms downstream systems, resulting in 429 (Too Many Requests) errors, exhausted connection pools, and a completely collapsed RAG pipeline.
This article provides an end-to-end architectural and code-level guide to taming concurrency spikes and preventing downstream throttling in an enterprise multi-agent LangGraph RAG system.
The Use Case: "OmniCare AI"
OmniCare AI is an enterprise-grade, multi-agent customer support platform for a major B2B SaaS provider.
The Architecture: API Gateway →→ AWS Lambda →→ LangGraph Multi-Agent Orchestrator.
The Agents:
Supervisor Agent: Routes the query based on intent.
Knowledge Agent: Performs RAG over product documentation (Amazon OpenSearch).
Action Agent: Executes account lookups and billing adjustments via internal REST APIs.
The Spike Scenario: A critical bug in the billing module causes a 40x spike in support tickets. Lambda scales from 10 to 1,000 concurrent executions in minutes.
The Fallout: The internal Billing API rate limit (100 req/sec) is breached. The OpenSearch connection pool is exhausted. The LLM provider (Amazon Bedrock) Tokens-Per-Minute (TPM) limits are hit. The graph nodes fail, and customers receive generic error messages.
![4]()
Anatomy of the Failure
To fix the problem, we must understand the cascade of failures during a spike:
The Lambda Explosion: API Gateway invokes Lambda synchronously. Lambda spins up hundreds of execution environments.
The Connection Stampede: Each new Lambda environment initializes its own OpenSearch HTTP client and internal API HTTP client. 1,000 Lambdas = 1,000 concurrent TCP connections, instantly overwhelming the downstream connection limits.
The LLM Bottleneck: 1,000 concurrent agents simultaneously request the primary LLM (e.g., Claude 3 Opus). The account's TPM/RPM limits are breached, throwing ThrottlingException.
The Cascading Timeout: Because the LLM and DB calls fail or hang, the Lambda execution time exceeds the API Gateway timeout (29 seconds), resulting in 504 Gateway Timeouts.
End-to-End Mitigation Strategy
We must implement defenses at three distinct layers: Ingestion/Compute, LLM Orchestration, and Downstream Execution.
Layer 1: Ingestion & Compute Throttling (The AWS Layer)
You must decouple the ingress traffic from the compute layer to absorb spikes and enforce strict concurrency limits.
Shift to Asynchronous SQS Ingestion: Never let API Gateway invoke heavy RAG Lambdas synchronously. API Gateway should push payloads to an Amazon SQS queue.
Event Source Mapping (ESM) Concurrency Control: Configure the Lambda Event Source Mapping to pull from SQS. Use the MaximumConcurrency scaling configuration to hard-cap the number of concurrent Lambda executions processing the queue. This acts as a shock absorber.
Reserved Concurrency (For Sync Fallbacks): If you must have a synchronous path, apply Reserved Concurrency to the Lambda function. This guarantees a maximum ceiling, ensuring Lambda cannot scale out of control and DDoS your own downstreams.
Dead Letter Queues (DLQ): Configure the SQS queue with a DLQ and a maxReceiveCount of 3. If a message fails due to a downstream outage, it is quarantined for later replay, preventing infinite retry loops that waste compute.
Layer 2: Resilient LLM Orchestration (The LangGraph Layer)
LangGraph nodes must be defensive against LLM provider throttling.
Model Fallbacks: Never rely on a single LLM. Use LangChain's with_fallbacks() to route to a smaller, faster, and higher-limit model (e.g., Claude 3 Haiku) if the primary model is throttled.
Semantic Caching: Implement a caching layer (like Redis or GPTCache) before the RAG retrieval node. If 500 users ask "How do I reset my password?", the cache serves 499 of them without ever hitting the LLM or Vector DB.
Layer 3: Defensive Downstream Execution (The Code Layer)
When calling internal APIs or Vector DBs, your code must handle backpressure gracefully.
Exponential Backoff with Jitter: Use the tenacity library for all downstream HTTP calls. Jitter is critical; without it, all failing Lambdas will retry at the exact same millisecond, causing a secondary spike.
Circuit Breakers: If the downstream API is consistently failing, trip a circuit breaker to fail fast and return a graceful degradation message, rather than holding Lambda compute hostage.
Connection Pooling: Ensure your Vector DB and HTTP clients are initialized in the global scope (as covered in the cold start article) so they are reused across warm invocations, preventing connection stampedes.
End-to-End Code Implementation
Below is the implementation of the AWS infrastructure and the resilient LangGraph multi-agent system.
1. Infrastructure: SQS Decoupling & Concurrency Control (Terraform)
This Terraform code sets up the SQS queue and configures the Lambda Event Source Mapping to strictly limit concurrency, protecting the downstreams.
# 1. The Dead Letter Queue for failed RAG tasks
resource "aws_sqs_queue" "omnicare_dlq" {
name = "omnicare-rag-dlq"
message_retention_seconds = 1209600 # 14 days
}
# 2. The Main Ingestion Queue
resource "aws_sqs_queue" "omnicare_rag_queue" {
name = "omnicare-rag-queue"
visibility_timeout_seconds = 300 # Must be > Lambda timeout
redrive_policy = jsonencode({
deadLetterTargetArn = aws_sqs_queue.omnicare_dlq.arn
maxReceiveCount = 3
})
}
# 3. Lambda Event Source Mapping (The Shock Absorber)
resource "aws_lambda_event_source_mapping" "sqs_to_lambda" {
event_source_arn = aws_sqs_queue.omnicare_rag_queue.arn
function_name = aws_lambda_function.omnicare_agent.arn
enabled = true
# Batch processing tuning
batch_size = 5
maximum_batching_window_in_seconds = 2
# CRITICAL: Hard cap on concurrent Lambda executions pulling from this queue.
# This prevents the "Thundering Herd" from overwhelming OpenSearch and Bedrock.
scaling_config {
maximum_concurrency = 150
}
}
2. Application: Resilient Multi-Agent LangGraph (Python)
This code demonstrates a multi-agent graph with LLM fallbacks and defensive downstream API execution.
import os
import httpx
import functools
from typing import TypedDict, Annotated, Literal
from tenacity import retry, wait_exponential_jitter, stop_after_attempt, retry_if_exception_type
from langgraph.graph import StateGraph, END
from langchain_aws import ChatBedrock
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.exceptions import OutputParserException
# ==========================================
# 1. GLOBAL SCOPE: Initialize Clients & Models
# ==========================================
# Primary LLM (High capability, lower rate limits)
primary_llm = ChatBedrock(
model_id="anthropic.claude-3-opus-20240229-v1:0",
region_name="us-east-1"
)
# Fallback LLM (Lower capability, much higher rate limits)
fallback_llm = ChatBedrock(
model_id="anthropic.claude-3-haiku-20240307-v1:0",
region_name="us-east-1"
)
# RESILIENCE PATTERN 1: LLM Fallbacks
# If Opus is throttled (429), LangChain automatically retries with Haiku.
resilient_llm = primary_llm.with_fallbacks([fallback_llm])
# ==========================================
# 2. STATE DEFINITION
# ==========================================
class AgentState(TypedDict):
messages: list
current_agent: str
context: dict
# ==========================================
# 3. RESILIENT DOWNSTREAM EXECUTION
# ==========================================
class RateLimitError(Exception):
"""Custom exception for downstream API throttling."""
pass
# RESILIENCE PATTERN 2: Exponential Backoff with Jitter
@retry(
retry=retry_if_exception_type(RateLimitError),
wait=wait_exponential_jitter(initial=1, max=10), # Jitter prevents secondary spikes
stop=stop_after_attempt(4)
)
def call_internal_billing_api(user_id: str, action: str):
"""Calls downstream microservice with defensive retries."""
response = httpx.post(
"https://internal-api.omnicare.com/billing",
json={"user_id": user_id, "action": action},
timeout=5.0
)
if response.status_code == 429:
raise RateLimitError("Downstream Billing API throttled")
if response.status_code != 200:
raise Exception(f"API Error: {response.status_code}")
return response.json()
# ==========================================
# 4. LANGGRAPH NODES
# ==========================================
def supervisor_node(state: AgentState):
"""Routes the query to the appropriate specialist agent."""
# In a real app, use an LLM call to classify intent.
# Hardcoded here for brevity.
query = state["messages"][-1].content.lower()
if "billing" in query or "invoice" in query:
return {"current_agent": "action_agent"}
return {"current_agent": "knowledge_agent"}
def knowledge_agent_node(state: AgentState):
"""RAG Node using the resilient LLM."""
query = state["messages"][-1].content
# 1. Retrieve from OpenSearch (Ensure connection pooling is configured globally)
# docs = opensearch_client.search(...)
# 2. Synthesize using the resilient LLM (Automatically falls back to Haiku if throttled)
prompt = f"Answer based on context: {docs}\n\nQuery: {query}"
response = resilient_llm.invoke(prompt)
return {"messages": [response], "current_agent": "end"}
def action_agent_node(state: AgentState):
"""Action Node executing downstream API calls with retries."""
query = state["messages"][-1].content
# Extract user intent (simplified)
user_id = "user_12345"
# This call will automatically retry with jitter if the downstream API returns 429
try:
result = call_internal_billing_api(user_id, "get_invoice_status")
response_msg = f"I checked your account. {result['message']}"
except Exception as e:
# Circuit breaker / Fail fast after retries are exhausted
response_msg = "I'm experiencing high demand and couldn't reach the billing system. Please try again in a moment."
return {"messages": [HumanMessage(content=response_msg)], "current_agent": "end"}
# ==========================================
# 5. GRAPH COMPILATION
# ==========================================
def route_to_agent(state: AgentState) -> Literal["knowledge_agent", "action_agent"]:
return state["current_agent"]
workflow = StateGraph(AgentState)
workflow.add_node("supervisor", supervisor_node)
workflow.add_node("knowledge_agent", knowledge_agent_node)
workflow.add_node("action_agent", action_agent_node)
workflow.set_entry_point("supervisor")
workflow.add_conditional_edges("supervisor", route_to_agent)
workflow.add_edge("knowledge_agent", END)
workflow.add_edge("action_agent", END)
# Compile the graph
app = workflow.compile()
# ==========================================
# 6. LAMBDA HANDLER
# ==========================================
def lambda_handler(event, context):
# Process SQS records
for record in event['Records']:
payload = json.loads(record['body'])
user_query = payload['query']
initial_state = {
"messages": [HumanMessage(content=user_query)],
"current_agent": "",
"context": {}
}
# Execute the graph
final_state = app.invoke(initial_state)
# Send response back to user (e.g., via WebSocket or API Gateway Callback)
send_response_to_user(payload['callback_url'], final_state['messages'][-1].content)
return {"statusCode": 200}
Observability: Proving the Mitigations Work
You cannot manage what you do not measure. To ensure your concurrency and throttling controls are effective:
AWS CloudWatch (Lambda & SQS):
Monitor ApproximateNumberOfMessagesVisible on the SQS queue. If this grows indefinitely, your MaximumConcurrency is too low for the sustained load.
Monitor Throttles on the Lambda function. If this spikes, your Reserved Concurrency (if used) is too low.
LangSmith:
Enable LangSmith tracing. Look specifically at the LLM Retries metric. If you see a high volume of fallbacks to Haiku, it means your Bedrock TPM limits for Opus are being hit, and you need to request a quota increase from AWS.
Downstream APM (Datadog/New Relic):
Track the P99 latency of the internal Billing API. If the P99 latency spikes exactly when Lambda concurrency spikes, your downstream API needs to be scaled up, or your Lambda MaximumConcurrency needs to be lowered.
Conclusion
Building an enterprise multi-agent LangGraph RAG system requires a shift in mindset from "making it work" to "making it survive." By decoupling ingestion via SQS, strictly limiting Lambda concurrency via Event Source Mappings, implementing LLM fallbacks, and wrapping downstream calls in defensive retry logic with jitter, you transform a fragile pipeline into a resilient enterprise platform. When the next traffic spike hits, your system won't collapse under the thundering herd; it will gracefully absorb the shock, degrade performance predictably, and keep your users informed.