Surviving Traffic Spikes in Multi-Agent Systems with a Three-Layer Contract
Keeping multi-agent systems up when traffic jumps tenfold: a queue gatekeeper, per-tool throttling and synchronous policy checks, plus metrics and a spike demo.
This article is published in English.
Sources: Multi-Agent Orchestration in Production: Handling Traffic Spikes at Scale
An agent graph can run happily at 10 requests per second and fall apart at 100. Requests pile up, policy checks get skipped, tool calls fight each other, and a system that seemed dependable turns into a liability. The agents are rarely the culprit. What is missing is an explicit agreement between the queue, the orchestration layer and the guardrails. Most teams only discover that gap while live traffic is already hitting it.
Why traffic spikes arrive sooner than expected
Agent systems are leaving the demo stage and entering real production, so spikes are guaranteed: a flash sale, a feature going viral, a scheduled batch job, or simply every user hitting the API on Tuesday morning. Two things happen at once as load rises.
First, routing takes real time. Choosing which agent handles a request, or deciding whether to queue it right now, is measurable work.
Second, systems tend to fail open under pressure. That usually isn't panicked engineers bypassing checks. It is naive error handling that defaults to "allow" on a timeout, or fallback routing that quietly skips the validation middleware. When policy enforcement vanishes under load, that architectural anti-pattern is normally the cause, not human intervention.
The cure is a clear, enforceable agreement among three parts: the queue (what gets in), the orchestration graph (where it goes) and the policy engine (what it may do). Settle that agreement early, and a spike becomes a solved problem instead of an emergency.
A contract with three layers
Layer 1: the queue as gatekeeper
The queue is the first place to enforce rules. Before any request reaches an agent, it passes a contract check covering three things:
- Priority. Requests are not equal. Interactive user queries can outrank batch jobs, and a paying customer can jump ahead of a free-tier one. The queue should handle that ordering so agents never need to know about it.
- Rate limits and quotas. Saying no is cheapest before any compute is spent. A user who has hit a daily API limit should get a clear rejection and not a place in line, which costs far less than a trip through an agent.
- Schema adherence. Input the system cannot process should be refused here and not discovered inside an agent.
What this layer promises is that every request sitting in the queue has been validated, ranked and approved to consume resources.
Layer 2: orchestration decides routing and pace
After a request has been queued, orchestration chooses its destination and what it is allowed to do. Concurrency and tool access are controlled here.
The routing contract answers a few questions. Which group of agents should own the request, for example fast agents for simple queries and slower ones for complex reasoning? How many requests can each agent handle at the same moment? What happens when every agent is busy: queue, reject or fall back?
The tool throttling contract recognizes that tools differ. Calling an external API a hundred times a second is nothing like running a local embedding. Each tool gets its own concurrency limit. A database-backed tool might allow ten simultaneous calls, while a tool that hits a third-party API might allow five. When a tool hits its ceiling, the request waits in that tool's own queue and not in the main one.
To keep those tool queues healthy and stop failures from cascading, production-grade primitives help:
- Circuit breakers and token buckets fail fast at the tool layer, preventing retry storms when a dependency is saturated.
- Dead-letter queues park malformed or poisoned tool payloads, so a single bad request never stops the whole agent loop.
- Leaky buckets kept per tool apply each tool's own rate limit, which protects parallel agent runs from outside constraints.
With these in place, bottlenecks at the tool level stay contained and visible and don't spread into system-wide instability. The layer's promise: a request goes to an agent with spare capacity, every tool honors its concurrency cap, and if a tool is saturated, the request waits for the tool and not for the agent.
Layer 3: policies that never bend
The hard part is that policies must run every single time, including under load. There is no "check it later" and no "this one looks safe."
The policy contract has four clauses:
- A policy check runs before any tool call, synchronously and blocking.
- The check is cheap, a few milliseconds at most. If it is not, the design is wrong.
- When a check fails, the request fails. The agent gets no chance to retry or negotiate.
- Failures are logged and watched, and a climb in policy failures is itself a signal.
Typical policies look like this: the agent may use only tools tagged read-only for this user; a tool may run only for an authenticated caller; an agent may make at most five tool calls per request. The layer promises that a check happens before every tool call, and that policies are fast, deterministic and absolute.
The shape of the pipeline
In a minimal pseudo-graph, a request enters through the queue gatekeeper, passes to the router, reaches an agent, and each tool call goes through policy enforcement and a per-tool throttle before executing.
[ Incoming Request ]
│
▼
[ Queue Gatekeeper ]
• Validate Schema
• Enforce Rate Limits
• Assign Priority
│ (Pass)
▼
[ Orchestration Router ]
• Check Agent Capacity
• Route to Target Node
│
▼
[ Tool-Level Gate ]
• Sync Policy Check
• Per-Tool Concurrency Limits
• Circuit Breaker / DLQ
│ (Pass)
▼
[ Tool Execution ]
The important detail is where policies attach. They apply at the tool level and not the agent level, which stops an agent from accidentally sidestepping them by using a tool in an unexpected way.
A spike scenario, with and without the contract
Imagine a customer support agent that can look up orders, process refunds and send emails, and traffic suddenly rises tenfold.
Without a contract, the failure unfolds in steps:
- Incoming requests exhaust the pool of available agents.
- Agents start queuing tool calls, and the send_email tool accumulates 500 pending calls.
- The email service begins timing out.
- Agents retry, so 1,000 calls are now waiting.
- Fail-open paths trigger. Timeouts or overloaded fallback routes bypass policy enforcement to keep the system available.
- An agent sends a refund email to the wrong customer.
With a contract, the same spike plays out differently:
- The queue gatekeeper regulates admission by priority and quota.
- The router sees that the send-email tool has reached its concurrency limit of ten, and it queues further email requests at the tool and not at the agent.
- Policy checks keep running on each tool call. If an agent tries to send a refund to the wrong customer, the check fails and the request is rejected.
- The system stays stable. Latency climbs, but reliability does not drop.
Putting it together
Start from LangGraph
A team already on LangGraph holds most of the primitives. Agents and tools are nodes, edges are routing choices, and conditional edges route on state. What needs adding is a queue in front, whether Redis, RabbitMQ or a plain in-memory queue, and policy middleware wrapped around tool calls.
The policy wrapper itself can be a small decorator that runs the checks and raises a violation error before the tool function ever executes.
from functools import wraps
from typing import Callable, Any
class PolicyViolation(Exception):
"""Raised when policy check fails."""
pass
def enforce_tool_policy(
policy_fn: Callable[[str, dict], bool]
):
"""Sync tool execution guardrail."""
def decorator(func: Callable):
@wraps(func)
def wrapper(*args, **kwargs):
tool_name = func.__name__
context = kwargs.get(
"request_context", {}
)
# Must fail closed
if not policy_fn(tool_name, context):
raise PolicyViolation(
f"Denied: {tool_name}"
)
return func(*args, **kwargs)
return wrapper
return decorator
# Example Usage
def strict_policy_check(tool_name: str, context: dict) -> bool:
if not context.get("is_authenticated"):
return False
if (context.get("role") != "admin" and tool_name.startswith("execute_")):
return False
return True
@enforce_tool_policy(strict_policy_check)
def execute_refund(
order_id: str,
amount: float,
request_context: dict = None
):
# Downstream API call
return f"Refund ${amount} sent: {order_id}"
Measure it
Implementing this and hoping is not enough. Instrument it and watch:
- Queue depth at each layer.
- How often, and which, tools hit their limits.
- Whether policies are rejecting requests, and why.
- The p50, p95 and p99 latencies, tracked separately for different request types.
- Cost per request under load. If it rises, some inefficiency is hiding.
A skeptical view
It is fair to say this looks like a lot of infrastructure for a problem not yet present. A system that handles ten requests per second and will stay there does not need it. A feature that might go viral, or enterprise customers with unpredictable traffic, are a different story, and the design belongs in place before launch.
Building it in advance costs little next to retrofitting it after a spike has broken the system. It also shouldn't be overbuilt. A simple queue, a policy check function and per-tool concurrency limits resolve most spike problems. Neither a distributed consensus algorithm nor a machine-learning load predictor is needed. What matters is clarity and constraints.
Main points
- Spikes expose design problems. When an agent system breaks under load, the agents are not too slow. Nobody set clear rules for how the queue, the routing and the policies fit together.
- The queue doubles as an enforcement point. It can validate, prioritize and reject requests so compute is not wasted.
- Policies are absolute and non-negotiable. They run at the tool level, synchronously, every time, with no exceptions.
- Throttle tools before agents. Bottlenecks usually sit downstream, in external APIs or databases that feed the agent context, and not in the LLM provider.
- Measure everything: queue depth, tool concurrency, policy failures, latency percentiles and cost per request. What is not measured cannot be improved.
Where to begin
- Write down the queue contract: what is accepted and what is rejected.
- Write down the routing contract: which agents own which tasks, and what the concurrency limits are.
- Write down the policy contract: which checks run before every tool call.
- Build it with something like LangGraph, add a queue layer and wrap the tools in policy checks.
- Load-test it. Simulate a tenfold spike and watch what breaks.
All of this is best done before production, while there is still time to be calm about it.