A healthcare tech company shipped a multi-agent customer service system in early 2025. By Q3, 40% of complex queries were failing silently. The orchestrator was calling subagents that called other subagents, context was being lost between hops, and token costs had tripled. They found out when their CSAT dropped 18 points and they started auditing conversation logs.
This matches a pattern that's shown up consistently across 2026 deployment post-mortems: nearly half of multi-agent CX pilots fail within six months of going to production. Not because the AI isn't capable. Because teams don't understand how multi-agent systems break -- and the breakage is silent until it's catastrophic.
Multi-agent systems fail in five specific ways. Each one has a diagnostic signature. This article walks through each failure mode, what it looks like in logs, what causes it, and how to fix it before it costs you a CSAT point.
Why multi-agent systems break differently
A single agent fails visibly. A tool returns an error, the model can't answer a question, a context limit is hit -- these failures produce observable signals. You can see them in logs, set alerts on them, and trace them to a cause.
Multi-agent systems fail systemically. An error in one agent becomes an input to another agent, which treats it as valid data and reasons from it. Context is handed off between agents where it can be silently truncated or corrupted. Failures are distributed across multiple components that log independently, with no shared reference tying them together. By the time the failure reaches the user, it looks like a bad answer from the final agent, with no visible trace of the three-hop chain that produced it.
The architecture matters too. 2026 production data from multiple orchestration framework teams converges on an uncomfortable finding: multi-agent systems consume an average of 15 times more tokens than equivalent single-agent or chat interactions. That multiplier exists because orchestrators process full subagent responses alongside original context, parallel agent calls multiply simultaneous compute, and retry logic without cost controls compounds every failure.
Failure mode 1: context collapse
Context collapse happens when a multi-agent workflow exhausts the context window of one or more agents in the chain, causing either a hard limit error or silent truncation of the most important information. It's the most common multi-agent failure mode and the one teams almost never instrument for until it hits them.
The setup: an orchestrator receives a long customer conversation, assembles a context payload (history, retrieved documents, tool results), and sends it to a subagent. The subagent does its work and returns a verbose result. The orchestrator incorporates that result alongside its existing context. Repeat this four or five times across a complex query, and either the orchestrator's context limit is hit, or earlier parts of the conversation are truncated silently.
What it looks like in logs: sudden drops in completion quality on long conversations, truncation errors appearing in specific agents, customers reporting that the agent "forgot" what they said earlier in the call.
The diagnostic: add token counting to every inter-agent handoff. Log the token count of each message sent to each agent alongside the agent's context limit. When usage consistently exceeds 80% of the limit, you're approaching collapse.
interface AgentHandoff {
agentId: string;
payload: {
context: string;
instructions: string;
toolResults?: Record<string, unknown>;
};
}
interface HandoffResult {
trimmed: AgentHandoff;
originalTokens: number;
finalTokens: number;
wasCompressed: boolean;
}
// Approximate token count: 1 token ~= 4 chars for English prose
function estimateTokens(text: string): number {
return Math.ceil(text.length / 4);
}
const AGENT_CONTEXT_LIMITS: Record<string, number> = {
'orchestrator': 180_000,
'specialist-a': 90_000,
'specialist-b': 90_000,
};
const CONTEXT_BUDGET_RATIO = 0.75; // Use max 75% for context, leave room for response
export function prepareHandoff(
handoff: AgentHandoff,
conversationHistory: string
): HandoffResult {
const limit = AGENT_CONTEXT_LIMITS[handoff.agentId] ?? 90_000;
const budget = Math.floor(limit * CONTEXT_BUDGET_RATIO);
const fullPayload = JSON.stringify(handoff.payload);
const originalTokens = estimateTokens(fullPayload);
if (originalTokens <= budget) {
return {
trimmed: handoff,
originalTokens,
finalTokens: originalTokens,
wasCompressed: false,
};
}
// Compress: summarize history, keep instructions and recent tool results verbatim
const historyTokenBudget = budget - estimateTokens(handoff.payload.instructions) - 2_000;
const compressedHistory = truncateToTokenBudget(conversationHistory, historyTokenBudget);
const trimmed: AgentHandoff = {
agentId: handoff.agentId,
payload: {
context: compressedHistory,
instructions: handoff.payload.instructions,
toolResults: handoff.payload.toolResults,
},
};
const finalPayload = JSON.stringify(trimmed.payload);
return {
trimmed,
originalTokens,
finalTokens: estimateTokens(finalPayload),
wasCompressed: true,
};
}
function truncateToTokenBudget(text: string, tokenBudget: number): string {
const charBudget = tokenBudget * 4;
if (text.length <= charBudget) return text;
// Keep the tail -- most recent context is most relevant
return `[Earlier context summarized for length]\n...\n${text.slice(-charBudget)}`;
}The fix for chronic context collapse is designing an explicit schema for each inter-agent handoff. Define exactly what information the subagent needs, not the full accumulated context. An orchestrator that passes a structured summary with key facts is far more reliable than one that passes the raw conversation history verbatim.
Failure mode 2: error propagation
Error propagation is the multi-agent failure mode that costs the most in production because it produces confident wrong answers, not visible errors.
Here's the pattern: a subagent encounters an error -- a tool returns a 404, a rate limit fires, a lookup returns empty results. The subagent generates a natural-language summary of what happened ("I wasn't able to find an account matching that information"). The orchestrator receives this message, treats it as a subagent response about the task, and incorporates it into its reasoning. If the orchestrator isn't specifically checking for error signals, it continues as if the subagent succeeded.
The result is an agent that confidently tells the customer "I've looked into your account and..." based on a lookup that never actually completed.
What it looks like in logs: successful orchestrator completions that correlate with customer complaint spikes, subagent error rates that don't match customer-visible failure rates (they're higher), conversations where the orchestrator references data it never actually retrieved.
The fix: structure every subagent response as a typed result, not a free-text message. Errors must be structurally distinguishable from successes.
// All subagent responses follow this contract
type SubagentResult<T> =
| { status: 'success'; data: T }
| { status: 'error'; code: string; message: string; retryable: boolean }
| { status: 'empty'; reason: string }; // Successful lookup, no results
// Example: account lookup subagent response
interface AccountData {
accountId: string;
status: string;
tier: string;
recentOrders: Array<{ orderId: string; status: string; date: string }>;
}
type AccountLookupResult = SubagentResult<AccountData>;
// Orchestrator handler that explicitly branches on result status
async function handleAccountLookup(
customerId: string,
orchestratorContext: string
): Promise<string> {
const result: AccountLookupResult = await callSubagent('account-specialist', {
task: 'lookup-account',
customerId,
});
switch (result.status) {
case 'success':
return composeResponseWithData(orchestratorContext, result.data);
case 'empty':
// Legitimate -- account doesn't exist or doesn't match
return `I wasn't able to find an account matching that information. ${result.reason}`;
case 'error':
if (result.retryable) {
// Retry once, then escalate
return await retryOrEscalate(customerId, orchestratorContext);
}
// Non-retryable: escalate to human
return triggerHumanHandoff('account-lookup-failure', result.message);
}
}Structured responses also enable better monitoring and alerting. When subagent errors are typed and explicit, you can set alerts on specific error codes, track retryable vs. non-retryable error ratios, and catch patterns that indicate systemic problems (a tool that starts returning errors for 20% of lookups instead of its baseline 1%).
Failure mode 3: runaway token costs
Cost spikes in multi-agent systems aren't gradual -- they're sudden. A query type that previously cost $0.03 gets routed to an orchestration pattern that calls five agents in sequence, each processing full context, and the cost is $0.47 per conversation. If that query type accounts for 30% of your traffic, the monthly bill doubles before anyone notices.
The multiplier effect is real: orchestrators process the full conversation plus all subagent responses, parallel agent calls multiply simultaneous compute, and retry logic amplifies every failure's cost. One subagent error that triggers three retries across a four-agent chain can cost 10-15x a successful single-pass query.
What it looks like in logs: sudden cost spikes correlating with specific query types or user segments, p99 token usage dramatically higher than p50 (outliers are very expensive), subagent call counts per conversation higher than expected.
The fix is explicit cost controls at the agent level, not just at the account level:
interface ConversationBudget {
conversationId: string;
maxTokens: number;
usedTokens: number;
agentCalls: number;
maxAgentCalls: number;
}
class MultiAgentCostController {
private budgets = new Map<string, ConversationBudget>();
startConversation(conversationId: string, tier: 'standard' | 'premium') {
this.budgets.set(conversationId, {
conversationId,
maxTokens: tier === 'premium' ? 200_000 : 80_000,
usedTokens: 0,
agentCalls: 0,
maxAgentCalls: tier === 'premium' ? 20 : 8,
});
}
async callSubagent<T>(
conversationId: string,
subagentFn: () => Promise<{ result: T; tokensUsed: number }>
): Promise<T> {
const budget = this.budgets.get(conversationId);
if (!budget) throw new Error(`No budget found for conversation ${conversationId}`);
if (budget.agentCalls >= budget.maxAgentCalls) {
throw new Error('AGENT_CALL_LIMIT_EXCEEDED');
}
const remaining = budget.maxTokens - budget.usedTokens;
if (remaining < 5_000) {
throw new Error('TOKEN_BUDGET_EXHAUSTED');
}
const { result, tokensUsed } = await subagentFn();
budget.usedTokens += tokensUsed;
budget.agentCalls += 1;
// Warn when approaching limits
if (budget.usedTokens > budget.maxTokens * 0.8) {
console.warn(`Conversation ${conversationId} at ${Math.round(budget.usedTokens / budget.maxTokens * 100)}% token budget`);
}
return result;
}
getUsage(conversationId: string): ConversationBudget | undefined {
return this.budgets.get(conversationId);
}
}The other essential control: define which queries should NOT go to a multi-agent pipeline. Simple queries (order status, store hours, policy questions) that can be answered by a single agent should never enter an orchestration flow. Route based on query complexity classification before the orchestrator sees the message. See the features/scenarios page for how to test your routing classification accuracy before shipping it.
Failure mode 4: latency compounding
Each agent in a multi-agent chain adds latency: the time for the orchestrator to formulate the delegated request, the subagent's processing time, and the round-trip overhead between them. In a four-agent sequential chain with 800ms per step, you're looking at 3-4 seconds of agent processing before the user sees a response. For voice CX agents, that's a conversation-ending pause.
The compounding effect is worse when agents are sequential but don't need to be. Teams often default to sequential delegation because it's easier to reason about, even when the subagents could run in parallel.
What it looks like: high median conversation latency with low error rates, user drop-off on complex queries specifically, high p99 latency on conversations that trigger multi-step orchestration.
The fix is identifying which subagent calls in your chain are truly sequential (B depends on A's result) versus parallel (B and A are independent). Implement parallel execution for independent calls:
interface ParallelAgentResult {
accountData: AccountData | null;
orderHistory: OrderHistory | null;
knowledgeArticles: KnowledgeArticle[] | null;
}
async function gatherContextInParallel(
customerId: string,
query: string
): Promise<ParallelAgentResult> {
// These three subagents are independent -- run in parallel
const [accountResult, orderResult, knowledgeResult] = await Promise.allSettled([
callSubagent<AccountData>('account-specialist', { customerId }),
callSubagent<OrderHistory>('order-specialist', { customerId, query }),
callSubagent<KnowledgeArticle[]>('knowledge-specialist', { query }),
]);
return {
accountData: accountResult.status === 'fulfilled' ? accountResult.value : null,
orderHistory: orderResult.status === 'fulfilled' ? orderResult.value : null,
knowledgeArticles: knowledgeResult.status === 'fulfilled' ? knowledgeResult.value : null,
};
}
async function handleComplexQuery(customerId: string, query: string): Promise<string> {
// Parallel context gathering -- one round-trip instead of three
const context = await gatherContextInParallel(customerId, query);
// Sequential resolution -- synthesis depends on gathered context
const resolution = await callSubagent<Resolution>('resolution-specialist', {
query,
context,
});
return resolution.responseText;
}Track per-agent span duration in your observability pipeline. When the p95 span for a specific subagent is consistently higher than others, that's your bottleneck. Optimizing a single high-latency agent often has more impact than restructuring the whole chain.
Failure mode 5: observability blindspots
Multi-agent systems log independently by default, producing one log stream per agent with no shared identifier tying them together. A five-agent orchestration produces five separate streams. When something goes wrong, you're reconstructing the sequence manually by timestamp -- error-prone and time-consuming for every incident. This is the failure mode that makes all the others harder to fix.
What it looks like: support tickets that can't be traced to a specific failure, inability to reproduce reported issues, quality metrics that look fine in the orchestrator logs but don't match customer feedback.
The fix is a shared trace ID that propagates through every agent invocation in a conversation:
import { randomUUID } from 'crypto';
interface AgentTrace {
traceId: string; // Shared across all agents in one conversation
spanId: string; // Unique to this agent call
parentSpanId?: string; // The orchestrator's span ID
agentId: string;
startTime: number;
endTime?: number;
inputTokens?: number;
outputTokens?: number;
status?: 'success' | 'error' | 'empty';
errorCode?: string;
}
class MultiAgentTracer {
private activeTraces = new Map<string, AgentTrace[]>();
startConversationTrace(conversationId: string): string {
const traceId = `trace_${randomUUID()}`;
this.activeTraces.set(traceId, []);
return traceId;
}
startSpan(
traceId: string,
agentId: string,
parentSpanId?: string
): AgentTrace {
const span: AgentTrace = {
traceId,
spanId: `span_${randomUUID()}`,
parentSpanId,
agentId,
startTime: Date.now(),
};
const traces = this.activeTraces.get(traceId) ?? [];
traces.push(span);
this.activeTraces.set(traceId, traces);
return span;
}
endSpan(
span: AgentTrace,
result: { status: AgentTrace['status']; inputTokens: number; outputTokens: number; errorCode?: string }
): void {
span.endTime = Date.now();
span.status = result.status;
span.inputTokens = result.inputTokens;
span.outputTokens = result.outputTokens;
span.errorCode = result.errorCode;
// Emit to your observability platform
this.emit(span);
}
private emit(span: AgentTrace): void {
// Emit as structured log -- pick up by your log aggregator
console.log(JSON.stringify({
level: 'info',
event: 'agent_span',
traceId: span.traceId,
spanId: span.spanId,
parentSpanId: span.parentSpanId,
agentId: span.agentId,
durationMs: span.endTime ? span.endTime - span.startTime : undefined,
tokens: {
input: span.inputTokens,
output: span.outputTokens,
total: (span.inputTokens ?? 0) + (span.outputTokens ?? 0),
},
status: span.status,
errorCode: span.errorCode,
}));
}
}With shared trace IDs, querying for a specific conversation's full execution is a single log search. You can see which agents ran, in what order, how long each took, what they returned, and where the chain broke.

The diagnostic framework
When a multi-agent system is underperforming, use this sequence to identify which failure mode you're dealing with:
Start with the symptom. Is the problem high cost, high latency, wrong answers, or invisible failures?
- High cost with normal latency and quality: Failure mode 3 (runaway cost). Check token usage per agent call, retry rates, and which query types trigger expensive paths.
- High latency with normal cost and quality: Failure mode 4 (latency compounding). Profile per-agent span durations, identify sequential calls that could be parallel.
- Wrong answers despite agents completing: Failure mode 2 (error propagation). Check subagent response structure -- are errors structurally typed, or are they free-text that the orchestrator can't distinguish from valid results?
- Degrading quality on long conversations: Failure mode 1 (context collapse). Check token usage at each agent handoff, compare performance on short vs. long conversations.
- Failures you can't trace to a cause: Failure mode 5 (observability blindspot). Instrument trace IDs across all agents before doing any other diagnosis.
Multi-agent failures rarely come from one source. Context collapse makes errors more frequent, errors propagate to produce wrong answers, and without observability you can't tell which is happening. The diagnostic sequence matters: fix observability first, then you can see the other failures clearly.
Choosing the right architecture
The 2026 consensus from orchestration framework teams is clear: start with the hub-and-spoke supervisor pattern and specialize only when you have evidence that you need to.
In the supervisor pattern, one orchestrator handles all user requests and delegates to specialized subagents. Subagents return results to the orchestrator -- they don't call other agents. This eliminates recursive delegation chains, makes the call graph predictable, and centralizes failure handling in one place.
The patterns that don't survive production well for CX deployments:
- Peer-to-peer meshes where agents call each other directly: produces cycles, makes tracing nearly impossible, and creates unpredictable latency
- Deep hierarchies (orchestrator to subagent to sub-subagent): context loss compounds at each level, and the call depth makes debugging extremely hard
- Fully parallel pipelines without output validation: fast, but subagent results that are wrong or inconsistent propagate to the synthesis agent unchanged
If you're starting a new multi-agent deployment, start with a supervisor that calls maximum three to four specialized subagents. That's the pattern with the widest native support across orchestration frameworks, the most predictable failure behavior, and the easiest path to adding monitoring and quality scoring as you scale.
What to build before you scale
Before you add more agents to your system, make sure you have these in place:
- Shared trace IDs across all agent calls in a conversation -- non-negotiable for any production diagnosis
- Typed subagent responses that structurally distinguish success, error, and empty result
- Token budget enforcement at the conversation level, not just the account level
- Per-agent latency spans that let you profile each hop individually
- Query complexity routing that keeps simple queries out of the orchestration pipeline entirely
Most multi-agent failures aren't architecture problems. They're instrumentation problems. The system is doing something -- you just can't see what. Fix visibility first, and the right fixes become obvious.
See what your multi-agent system is actually doing
Chanl surfaces per-agent quality scores, trace data, and cost attribution across your full agent fleet — so you can identify which failure mode you're dealing with before it becomes a CSAT problem.
Explore agent monitoringCo-founder
Building the platform for AI agents at Chanl — tools, testing, and observability for customer experience.
El briefing de Signal
Un email por semana. Cómo los equipos líderes de CS, ingresos e IA están convirtiendo conversaciones en decisiones. Benchmarks, playbooks y lo que funciona en producción.

