Scaling Agent Infrastructure
It's Black Friday. Your AI shopping assistant has been handling 100 concurrent conversations smoothly. At 8 AM, traffic spikes to 10,000 concurrent conversations. Your carefully tuned agent starts timing out, queues back up, and customers see "Agent unavailable."
Scaling AI agents is fundamentally different from scaling traditional web services. Let's learn how to handle it.
The Scaling Challenge
Traditional web services have predictable latency - an API call takes ~50ms. You can calculate capacity:
Traditional: 1 server × (1000ms / 50ms) = 20 requests/second
AI agents break this model:
With 500ms-30s variance, traditional capacity planning fails.
Horizontal Scaling Architecture
from kubernetes import client, config
from dataclasses import dataclass
import asyncio
@dataclass
class ScalingConfig:
min_replicas: int = 3
max_replicas: int = 50
target_queue_depth: int = 10 # requests per pod
scale_up_threshold: float = 0.8
scale_down_threshold: float = 0.3
cooldown_seconds: int = 60
class AgentAutoScaler:
def __init__(self, config: ScalingConfig):
self.config = config
self.k8s = client.AppsV1Api()
self.metrics = MetricsClient()
async def evaluate_scaling(self):
"""Main scaling loop"""
while True:
metrics = await self.collect_metrics()
if self.should_scale_up(metrics):
await self.scale_up(metrics)
elif self.should_scale_down(metrics):
await self.scale_down(metrics)
await asyncio.sleep(10) # Check every 10 seconds
def should_scale_up(self, metrics: AgentMetrics) -> bool:
# Scale based on queue depth, not CPU
queue_per_pod = metrics.queue_depth / metrics.current_replicas
# Also consider p95 latency
latency_stressed = metrics.p95_latency > 5000 # 5s
return (
queue_per_pod > self.config.target_queue_depth * self.config.scale_up_threshold
or latency_stressed
) and metrics.current_replicas < self.config.max_replicas
async def scale_up(self, metrics: AgentMetrics):
# Calculate needed replicas based on queue depth
needed = math.ceil(
metrics.queue_depth / self.config.target_queue_depth
)
target = min(needed, self.config.max_replicas)
# Scale aggressively during spikes
if metrics.queue_growth_rate > 100: # Queue growing fast
target = min(target * 1.5, self.config.max_replicas)
await self._set_replicas("agent-deployment", int(target))
self.log_scaling_event("scale_up", metrics.current_replicas, target)
async def collect_metrics(self) -> AgentMetrics:
return AgentMetrics(
current_replicas=await self._get_replica_count(),
queue_depth=await self.metrics.get_queue_depth(),
p95_latency=await self.metrics.get_p95_latency(),
queue_growth_rate=await self.metrics.get_queue_growth_rate(),
active_conversations=await self.metrics.get_active_sessions()
)
Load Balancing Strategies
Strategy 1: Semantic Load Balancing
Route queries based on complexity to appropriate agent tiers:
from enum import Enum
from typing import Tuple
class AgentTier(Enum):
LIGHTWEIGHT = "lightweight" # GPT-4o-mini, no tools
STANDARD = "standard" # GPT-4o, basic tools
HEAVY = "heavy" # GPT-4, all tools, long context
class SemanticLoadBalancer:
def __init__(self):
self.classifier = QueryClassifier()
self.tier_pools = {
AgentTier.LIGHTWEIGHT: AgentPool(min_size=10, max_size=100),
AgentTier.STANDARD: AgentPool(min_size=5, max_size=50),
AgentTier.HEAVY: AgentPool(min_size=2, max_size=20),
}
async def route(self, query: str, context: dict) -> Tuple[AgentTier, Agent]:
# Classify query complexity
complexity = await self.classifier.analyze(query, context)
# Simple FAQ-style queries
if complexity.is_simple and complexity.confidence > 0.9:
tier = AgentTier.LIGHTWEIGHT
# Queries needing tools (search, order lookup)
elif complexity.needs_tools and not complexity.needs_reasoning:
tier = AgentTier.STANDARD
# Complex multi-step reasoning
else:
tier = AgentTier.HEAVY
# Get agent from appropriate pool
agent = await self.tier_pools[tier].acquire()
return tier, agent
class QueryClassifier:
"""Fast classifier for routing decisions"""
SIMPLE_PATTERNS = [
r"what('s| is) your return policy",
r"how (do i|can i) track",
r"what are your (hours|shipping)",
r"do you (ship|deliver) to",
]
async def analyze(self, query: str, context: dict) -> QueryComplexity:
# Check for simple patterns first (no LLM needed)
for pattern in self.SIMPLE_PATTERNS:
if re.search(pattern, query.lower()):
return QueryComplexity(
is_simple=True,
needs_tools=False,
needs_reasoning=False,
confidence=0.95
)
# Use fast model for ambiguous cases
classification = await self.fast_llm.classify(
query=query,
context_summary=self._summarize_context(context)
)
return classification
Strategy 2: Session Affinity with Spillover
Keep conversations on the same agent when possible, but spill over during load:
class AffinityLoadBalancer:
def __init__(self):
self.session_map = {} # session_id -> agent_id
self.agent_loads = {} # agent_id -> current_sessions
async def get_agent(self, session_id: str) -> Agent:
# Check if session has affinity
if session_id in self.session_map:
agent_id = self.session_map[session_id]
agent = self.agents.get(agent_id)
# Verify agent is still healthy and not overloaded
if agent and agent.is_healthy and agent.current_load < agent.max_load:
return agent
# Affinity broken - remove mapping
del self.session_map[session_id]
# Find least loaded agent
agent = self._get_least_loaded_agent()
self.session_map[session_id] = agent.id
return agent
def _get_least_loaded_agent(self) -> Agent:
healthy_agents = [a for a in self.agents.values() if a.is_healthy]
return min(healthy_agents, key=lambda a: a.current_load / a.max_load)
Handling Traffic Spikes
Pre-Scaling for Known Events
class EventBasedScaler:
"""Pre-scale for known traffic events"""
KNOWN_EVENTS = {
"black_friday": {
"start": "2024-11-29T00:00:00",
"end": "2024-11-30T23:59:59",
"scale_factor": 5.0,
"pre_scale_hours": 2
},
"cyber_monday": {
"start": "2024-12-02T00:00:00",
"end": "2024-12-02T23:59:59",
"scale_factor": 4.0,
"pre_scale_hours": 2
},
"flash_sale": {
"scale_factor": 3.0,
"pre_scale_hours": 1
}
}
async def schedule_pre_scaling(self):
for event_name, config in self.KNOWN_EVENTS.items():
if "start" in config:
pre_scale_time = (
datetime.fromisoformat(config["start"])
- timedelta(hours=config["pre_scale_hours"])
)
self.scheduler.schedule(
pre_scale_time,
self.pre_scale,
args=(event_name, config["scale_factor"])
)
async def pre_scale(self, event_name: str, scale_factor: float):
current = await self.get_current_replicas()
target = int(current * scale_factor)
self.logger.info(f"Pre-scaling for {event_name}: {current} -> {target}")
await self.set_replicas(target)
# Also warm up caches
await self.warm_caches()
# Pre-warm LLM connections
await self.warm_llm_connections(target)
Dynamic Degradation During Spikes
class AdaptiveDegradation:
"""Automatically adjust response quality based on load"""
def __init__(self):
self.thresholds = {
"normal": {"queue_depth": 50, "latency_p95": 3000},
"elevated": {"queue_depth": 200, "latency_p95": 5000},
"critical": {"queue_depth": 500, "latency_p95": 10000},
}
async def get_current_mode(self) -> DegradationMode:
metrics = await self.get_metrics()
if (metrics.queue_depth > self.thresholds["critical"]["queue_depth"]
or metrics.p95_latency > self.thresholds["critical"]["latency_p95"]):
return DegradationMode.CRITICAL
elif (metrics.queue_depth > self.thresholds["elevated"]["queue_depth"]
or metrics.p95_latency > self.thresholds["elevated"]["latency_p95"]):
return DegradationMode.ELEVATED
return DegradationMode.NORMAL
def get_agent_config(self, mode: DegradationMode) -> AgentConfig:
configs = {
DegradationMode.NORMAL: AgentConfig(
model="gpt-4o",
max_tokens=1000,
tools_enabled=True,
context_window=8000,
use_cache=True
),
DegradationMode.ELEVATED: AgentConfig(
model="gpt-4o-mini",
max_tokens=500,
tools_enabled=True,
context_window=4000,
use_cache=True,
cache_threshold=0.7 # Lower similarity threshold
),
DegradationMode.CRITICAL: AgentConfig(
model="gpt-4o-mini",
max_tokens=200,
tools_enabled=False, # No tool calls
context_window=2000,
use_cache=True,
cache_threshold=0.5,
fallback_to_templates=True
),
}
return configs[mode]
LLM Provider Load Balancing
Don't put all your eggs in one basket. Distribute across providers:
class MultiProviderBalancer:
"""Balance load across multiple LLM providers"""
def __init__(self):
self.providers = {
"openai": ProviderConfig(
weight=0.6, # Primary
rate_limit=10000, # RPM
cost_per_1k_tokens=0.03,
latency_p50=800
),
"anthropic": ProviderConfig(
weight=0.3, # Secondary
rate_limit=5000,
cost_per_1k_tokens=0.025,
latency_p50=900
),
"azure_openai": ProviderConfig(
weight=0.1, # Backup
rate_limit=8000,
cost_per_1k_tokens=0.03,
latency_p50=700
),
}
self.usage_tracker = UsageTracker()
async def select_provider(self, request: AgentRequest) -> str:
# Check rate limit headroom
available = []
for name, config in self.providers.items():
current_usage = await self.usage_tracker.get_current_rpm(name)
headroom = (config.rate_limit - current_usage) / config.rate_limit
if headroom > 0.1: # At least 10% headroom
available.append((name, config, headroom))
if not available:
raise AllProvidersExhausted()
# Weighted selection with headroom consideration
weights = [
config.weight * headroom
for name, config, headroom in available
]
total = sum(weights)
normalized = [w / total for w in weights]
return random.choices(
[name for name, _, _ in available],
weights=normalized
)[0]
async def call_with_fallback(self, request: AgentRequest) -> Response:
providers_tried = []
while len(providers_tried) < len(self.providers):
provider = await self.select_provider(request)
providers_tried.append(provider)
try:
return await self._call_provider(provider, request)
except (RateLimitError, TimeoutError, ProviderError):
continue
raise AllProvidersFailed(providers_tried)
Real-World: Amazon's Peak Handling
Amazon's AI assistants handle massive scale during Prime Day:
- Cell-based architecture: Traffic isolated to independent cells
- Predictive scaling: ML models predict traffic 30 mins ahead
- Tiered responses: Simple queries get cached responses
- Queue shedding: Drop lowest-priority requests under extreme load
Key Takeaways
- Scale on queue depth, not CPU - LLM latency variance makes CPU-based scaling unreliable
- Semantic load balancing - Route simple queries to lightweight agents
- Pre-scale for known events - Black Friday shouldn't be a surprise
- Multi-provider distribution - Don't depend on a single LLM provider
Next up: Cost Optimization Strategies - How to keep your AI agent costs under control.