Knowledge BaseAI Load Testing: Your LLM Endpoint Will Buckle Under Real TrafficAI QUALITY

AI Load Testing: Your LLM Endpoint Will Buckle Under Real Traffic

JK
James Kim · April 2026 · 5 min read

TL;DR

LLM endpoints have unique failure modes: streaming interruptions, token exhaustion, cascading timeouts Load testing must measure: latency percentiles, token throughput, cache hit rates, cost under load Streaming responses fail ungracefully unless explicitly handled, clients see partial responses Rate limiting must be intelligent: per-token, per-user, with graceful degradation Test at 3-5x expected traffic; most AI endpoints fail before traditional systems Cost modeling during peak load is critical, a traffic spike can cost thousands per hour

Why Traditional Load Testing Doesn't Work

Traditional load testing assumes request-response pairs: fast requests, small payloads, clear pass/fail semantics. LLM endpoints violate all these assumptions.

The Unique Challenges

Long-tailed latency: A typical API request takes 50ms. An LLM request takes 2-8 seconds. This changes everything. Your timeout defaults are wrong. Your queue strategy is wrong. Your retry logic cascades failures differently.

Streaming responses: Clients don't wait for a complete response. They start receiving tokens immediately. If the connection drops midway, the client has a partial response. Your monitoring needs to understand this.

Variable token consumption: You can't predict response length. Some prompts return 10 tokens. Others return 1000. This makes capacity planning nearly impossible and cost modeling a moving target.

Cascading failures: When one LLM API goes down, your retry logic hits it again. And again. You end up amplifying the failure instead of gracefully degrading.

Context window exhaustion: If you're using conversation history, each request consumes tokens just to send context. Under load, context windows fill faster. Quality degrades. Cost spirals.

The critical difference: Traditional APIs fail fast and fail hard. LLM endpoints fail slowly and fail soft. They degrade gracefully until they don't, then fail catastrophically. Your monitoring needs to catch the degradation before the catastrophe.

What to Measure During Load Testing

Latency Percentiles (Not Averages)

Average response time is useless. You need percentiles. The 50th percentile (median) tells you about typical users. The 95th percentile tells you about frustrated users. The 99th percentile tells you about your SLA.

import numpy as np
from concurrent.futures import ThreadPoolExecutor
import time

def load_test_llm_endpoint(endpoint, num_requests=1000, concurrency=50):
 latencies = []

 def make_request():
 start = time.time()
 try:
 response = endpoint.invoke(prompt="test")
 elapsed = time.time() - start
 latencies.append(elapsed)
 return elapsed
 except Exception as e:
 latencies.append(None) # Track failures
 return None

 with ThreadPoolExecutor(max_workers=concurrency) as executor:
 executor.map(make_request, range(num_requests))

 # Calculate percentiles
 valid_latencies = [l for l in latencies if l is not None]
 print(f"Median latency: {np.percentile(valid_latencies, 50):.2f}s")
 print(f"p95 latency: {np.percentile(valid_latencies, 95):.2f}s")
 print(f"p99 latency: {np.percentile(valid_latencies, 99):.2f}s")
 print(f"Max latency: {max(valid_latencies):.2f}s")
 print(f"Error rate: {(len(latencies) - len(valid_latencies)) / len(latencies) * 100:.1f}%")

Your p95 latency is what users experience. If it's 15 seconds, users will timeout. Your p99 latency is your worst-case SLA. If you promise 99% of requests within 20 seconds, you need p99 below 20s.

Token Throughput

Don't measure requests per second. Measure tokens per second. This is what matters for capacity and cost.

def measure_token_throughput(endpoint, duration_seconds=60, concurrency=50):
 total_tokens_consumed = 0
 total_tokens_generated = 0
 start_time = time.time()

 def consume_tokens():
 nonlocal total_tokens_consumed, total_tokens_generated
 while time.time() - start_time < duration_seconds:
 prompt = "Tell me about machine learning"
 result = endpoint.invoke(prompt=prompt)

 # Count tokens
 prompt_tokens = count_tokens(prompt)
 response_tokens = count_tokens(result.content)

 total_tokens_consumed += prompt_tokens
 total_tokens_generated += response_tokens

 with ThreadPoolExecutor(max_workers=concurrency) as executor:
 executor.map(consume_tokens, range(concurrency))

 elapsed = time.time() - start_time
 print(f"Input tokens/sec: {total_tokens_consumed / elapsed:.0f}")
 print(f"Output tokens/sec: {total_tokens_generated / elapsed:.0f}")
 print(f"Total tokens/sec: {(total_tokens_consumed + total_tokens_generated) / elapsed:.0f}")

This metric is critical. Your LLM provider bills you per token. Under load, token throughput is directly proportional to cost. If you're generating 10k tokens/second and that costs $0.10 per 1M tokens, you're burning $1/second under peak load.

Cache Hit Rates

If you're using prompt caching (smart move), measure hit rates under load. This is where you save money and latency.

class CachingLLMEndpoint:
 def __init__(self):
 self.cache = {}
 self.hits = 0
 self.misses = 0

 def invoke(self, prompt: str) -> str:
 cache_key = hash(prompt)

 if cache_key in self.cache:
 self.hits += 1
 return self.cache[cache_key]
 else:
 self.misses += 1
 result = self.llm.invoke(prompt)
 self.cache[cache_key] = result
 return result

 @property
 def cache_hit_rate(self) -> float:
 total = self.hits + self.misses
 return self.hits / total if total > 0 else 0.0

# Under load, monitor hit rate
endpoint = CachingLLMEndpoint()
# ... run load test ...
print(f"Cache hit rate: {endpoint.cache_hit_rate * 100:.1f}%")

# Healthy systems see 40-60% hit rates under normal load
# Below 20% suggests prompts are too varied
# Above 80% suggests queries are repetitive

Cost Projection

Track cost per request under load. This is the metric your CFO cares about.

def project_cost_under_load(tokens_per_second, cost_per_million_tokens=0.10):
 """Project hourly cost based on token throughput"""
 tokens_per_hour = tokens_per_second * 3600
 cost_per_hour = (tokens_per_hour / 1_000_000) * cost_per_million_tokens

 print(f"Tokens/second: {tokens_per_second:.0f}")
 print(f"Tokens/hour: {tokens_per_hour:.0f}")
 print(f"Cost/hour: ${cost_per_hour:.2f}")
 print(f"Cost/day (24h): ${cost_per_hour * 24:.2f}")
 print(f"Cost/month (30d): ${cost_per_hour * 24 * 30:.2f}")

 # Example: 1000 tokens/second at $0.10 per 1M tokens
 project_cost_under_load(1000)

At 1000 tokens/second, you're spending $25.92/hour. At 5000 tokens/second, you're spending $129.60/hour. This is why load testing matters. A 5x traffic spike becomes a $100/hour problem.

How AI Endpoints Fail

Failure Mode 1: Streaming Interruptions

The client receives tokens one by one. If the connection drops at token 500 of 1000, the client gets a partial response. Your application needs to handle this gracefully.

async def stream_with_resilience(endpoint, prompt: str, max_retries: int = 3):
 tokens_received = []

 for attempt in range(max_retries):
 try:
 async for token in endpoint.stream(prompt):
 tokens_received.append(token)
 except ConnectionError:
 # Connection dropped, retry with partial completion
 if attempt < max_retries - 1:
 await asyncio.sleep(2 ** attempt) # Exponential backoff
 continue
 else:
 # Final attempt failed
 break

 # Return best-effort response
 return "".join(tokens_received)

Streaming without retry logic is dangerous. Under load, network conditions degrade. Connections drop. You need explicit handling.

Failure Mode 2: Token Budget Exhaustion

Your LLM has a token limit (context window). If you're storing conversation history, each new request uses tokens just for context. Under load with long conversations, you hit the limit.

class ConversationWithTokenBudget:
 def __init__(self, max_context_tokens: int = 4000):
 self.history = []
 self.max_context_tokens = max_context_tokens

 def add_exchange(self, user_message: str, assistant_response: str):
 """Add conversation turn, evict old messages if needed"""
 new_tokens = count_tokens(user_message) + count_tokens(assistant_response)

 # Check if we're within budget
 current_tokens = sum(count_tokens(m) for m in self.history)
 while current_tokens + new_tokens > self.max_context_tokens:
 # Remove oldest message
 self.history.pop(0)
 current_tokens = sum(count_tokens(m) for m in self.history)

 self.history.append(user_message)
 self.history.append(assistant_response)

Without explicit token budget management, your system fails when context fills. Messages get dropped silently, or requests fail with mysterious errors.

Failure Mode 3: Cascading Timeouts

Your timeout is set to 30 seconds. Under load, latency hits 25 seconds. Your retry logic kicks in, making a second request. Now you're at 50 seconds. The client timeout fires. You've wasted capacity and frustrated the user.

def invoke_with_smart_retries(endpoint, prompt: str, base_timeout: float = 30.0):
 """Retry logic that doesn't cascade failures"""
 max_attempts = 2
 attempt = 1

 while attempt <= max_attempts:
 # Scale timeout by attempt number
 # First attempt: 30s, second attempt: 45s
 timeout = base_timeout * (1 + (attempt - 1) * 0.5)

 try:
 return endpoint.invoke(prompt, timeout=timeout)
 except TimeoutError:
 if attempt < max_attempts:
 attempt += 1
 else:
 raise

 raise TimeoutError("Max retries exceeded")

Smart retry logic waits longer on subsequent attempts. It doesn't assume the first timeout was a transient blip. It backs off and gives the system time to recover.

Failure Mode 4: Cost Explosion

Under load, context windows grow, retry logic kicks in, and you're generating tokens faster than ever. Your bill becomes 10x larger than expected.

class CostMonitor:
 def __init__(self, cost_limit_per_hour: float = 100.0):
 self.cost_limit = cost_limit_per_hour
 self.current_hour_cost = 0.0
 self.start_time = time.time()

 def track_request(self, input_tokens: int, output_tokens: int, cost_per_mtok: float = 0.001):
 """Track cost and alert if budget exceeded"""
 total_tokens = input_tokens + output_tokens
 cost = (total_tokens / 1_000_000) * (cost_per_mtok * 1000)
 self.current_hour_cost += cost

 if self.current_hour_cost > self.cost_limit:
 alert(f"Cost limit exceeded: ${self.current_hour_cost:.2f}/hour")

 elapsed_hours = (time.time() - self.start_time) / 3600
 if elapsed_hours >= 1.0:
 # Reset for next hour
 self.current_hour_cost = 0.0
 self.start_time = time.time()

Without cost monitoring, a traffic spike silently becomes a thousand-dollar incident. Monitor it explicitly.

Load Testing Strategies

Baseline Testing (1x expected load)

Test at your expected normal traffic. This establishes baselines for latency, throughput, and cost. You'll find 90% of issues here.

# Expected: 100 RPS, average response 3 seconds
load_test(
 endpoint=my_endpoint,
 requests_per_second=100,
 concurrency=300,
 duration_seconds=300 # 5 minute test
)

Stress Testing (3-5x expected load)

Push beyond expected limits. This is where you find failure modes. Most AI endpoints degrade gracefully up to 2-3x load, then fail hard at 4-5x.

# Expected: 100 RPS, stressing at 300-500 RPS
load_test(
 endpoint=my_endpoint,
 requests_per_second=400,
 concurrency=1200,
 duration_seconds=120 # Shorter test at higher load
)

Sustained Load Testing (8+ hours)

Long-running tests reveal memory leaks, connection pool exhaustion, and cache degradation. AI endpoints need this.

load_test(
 endpoint=my_endpoint,
 requests_per_second=150,
 concurrency=450,
 duration_seconds=28800 # 8 hours
)

# Monitor for:
# - Memory growth over time
# - Connection pool saturation
# - Cache hit rate degradation
# - Increasing latency percentiles

Designing Resilient AI Systems

1. Implement Circuit Breakers

When downstream services (LLM APIs) fail, stop sending requests. Return a degraded response instead.

class CircuitBreaker:
 def __init__(self, failure_threshold: int = 5, timeout_seconds: int = 60):
 self.failures = 0
 self.threshold = failure_threshold
 self.timeout = timeout_seconds
 self.last_failure_time = None
 self.state = "closed" # closed, open, half-open

 def call(self, func, *args, **kwargs):
 if self.state == "open":
 if time.time() - self.last_failure_time > self.timeout:
 self.state = "half-open"
 else:
 raise Exception("Circuit breaker open")

 try:
 result = func(*args, **kwargs)
 self.failures = 0
 self.state = "closed"
 return result
 except Exception as e:
 self.failures += 1
 self.last_failure_time = time.time()
 if self.failures >= self.threshold:
 self.state = "open"
 raise

2. Rate Limiting with Token Awareness

Limit by tokens, not requests. This prevents cost explosion.

class TokenRateLimiter:
 def __init__(self, tokens_per_second: int = 10000):
 self.limit = tokens_per_second
 self.current_tokens = tokens_per_second
 self.last_refill = time.time()

 def allow_request(self, estimated_tokens: int) -> bool:
 now = time.time()
 elapsed = now - self.last_refill

 # Refill at rate
 refill = elapsed * self.limit
 self.current_tokens = min(self.limit, self.current_tokens + refill)
 self.last_refill = now

 if self.current_tokens >= estimated_tokens:
 self.current_tokens -= estimated_tokens
 return True
 return False

3. Graceful Degradation

When your LLM endpoint is overloaded, return a cached response or a simpler model.

def invoke_with_fallback(primary_endpoint, fallback_endpoint, prompt: str):
 try:
 # Try primary (expensive, slow, high-quality)
 return primary_endpoint.invoke(prompt, timeout=10.0)
 except (TimeoutError, RateLimitError):
 # Fall back to cache or simpler model
 if cached_response := check_cache(prompt):
 return cached_response
 return fallback_endpoint.invoke(prompt) # Faster, cheaper, lower-quality
The real lesson: AI endpoints fail under load like traditional systems, but differently. Your timeout logic is wrong. Your retry strategy is wrong. Your cost model is wrong. Load testing reveals all of it.

The Bottom Line

Before you launch an AI-powered feature, load test it. Not "when you have time." Before. This is table stakes. Test at 3x expected load. Measure latency percentiles, token throughput, cache hit rates, and cost. Find the failure modes. Fix them.

Then deploy to production knowing you won't wake up at 3am to a bill for $50,000.

Ship AI With Confidence

alt.qa provides the testing infrastructure modern AI teams need. Practical evaluation, monitoring, and quality gates, all in one platform.

Try alt.qa Free →
James Kim James Kim writes about AI quality engineering at alt.qa, built by TheWorkCompany.