Stress Testing LLM-Powered Applications: Load, Latency, and Limits
Stress testing a traditional web application is well-understood: simulate concurrent users, ramp up load, find the breaking point. LLM-powered applications fail differently, and traditional stress testing approaches miss the important failure modes.
This guide covers stress testing strategies specific to applications that call LLM APIs — OpenAI, Gemini, Anthropic, or self-hosted models. The patterns are different from conventional load testing because the bottlenecks are different.
How LLM Applications Fail Under Load
Traditional applications fail under load when CPU, memory, or database connections saturate. LLM applications have different bottlenecks.
Rate limits. OpenAI, Gemini, and Anthropic all impose rate limits — requests per minute, tokens per minute, requests per day. These vary by tier and model. Under load, you'll hit rate limits before you saturate your own infrastructure. Rate limit errors are a different failure mode than CPU saturation, and your application needs to handle them correctly.
Latency spikes. LLM APIs have variable latency. A request that takes 2 seconds under normal load might take 20 seconds when the provider is under load. Your timeout configuration, loading UI, and user experience need to handle this range.
Context accumulation. If your application maintains conversation history, context grows with every turn. Under load, if users have long conversations, token usage per request increases over time. Your cost controls and context management code are stressed differently at high concurrency than they are for individual users.
Queue buildup. If you queue LLM requests (good practice — prevents hammering the API), queue depth grows under load. Response time increases. Users waiting for results need feedback. Your queue timeout handling is only exercised under load.
Cost. LLM calls cost money per token. Under load, costs scale linearly with requests. If your rate limiting or cost controls don't work correctly, a load spike can be expensive.
Setting Up an LLM Load Test
Standard load testing tools (k6, Locust, JMeter) work for LLM applications with modifications.
Don't hit the real LLM API in load tests. Rate limits will stop you before you generate meaningful load. Use a test double — a server that simulates LLM API responses with realistic latency.
Here's a minimal LLM API mock in Node.js:
const express = require('express');
const app = express();
app.use(express.json());
// Simulate OpenAI chat completions with variable latency
app.post('/v1/chat/completions', async (req, res) => {
const prompt = req.body.messages[req.body.messages.length - 1].content;
const tokenCount = prompt.split(' ').length;
// Simulate LLM latency: base 500ms + 10ms per token + jitter
const baseLatency = 500 + (tokenCount * 10);
const jitter = Math.random() * 2000;
await sleep(baseLatency + jitter);
res.json({
choices: [{
message: {
role: 'assistant',
content: generateMockResponse(tokenCount)
},
finish_reason: 'stop'
}],
usage: {
prompt_tokens: tokenCount,
completion_tokens: 100,
total_tokens: tokenCount + 100
}
});
});
app.listen(8080);Configure your application to hit this mock instead of OpenAI for load testing. Now you can generate realistic load without hitting rate limits.
Load Test Scenarios for LLM Applications
Scenario 1: Concurrent users
Ramp from 1 to 100 concurrent users over 5 minutes. Measure:
- P50, P95, P99 response times
- Queue depth over time
- Error rate
- Response time distribution (is it bimodal? timeouts?)
In k6:
export const options = {
stages: [
{ duration: '2m', target: 20 },
{ duration: '3m', target: 100 },
{ duration: '2m', target: 100 },
{ duration: '1m', target: 0 }
],
thresholds: {
http_req_duration: ['p95<10000'], // LLM calls are slow, 10s P95
http_req_failed: ['rate<0.01'] // Less than 1% error rate
}
};Scenario 2: Rate limit simulation
Configure your mock to return 429 after N requests per minute. Verify:
- Your retry logic works correctly
- Users see appropriate messages, not raw 429 errors
- Retries use exponential backoff
- The system recovers gracefully when rate limits reset
Scenario 3: Latency spike injection
Simulate latency spikes (mock returns responses in 30+ seconds) during a load test. Verify:
- Timeouts trigger correctly
- Users see timeout error messages, not hanging requests
- Timeout errors don't cascade into other failures
- The system returns to normal when latency normalizes
Scenario 4: Context window growth
Simulate long conversations where context grows with each turn. Start with 100 token prompts, grow to 50,000 tokens over the test. Verify:
- Cost controls (max context limits) trigger before hitting model limits
- Users get appropriate messages when context is too long
- Context pruning works if you implemented it
What to Assert
In a traditional load test, you assert response time and error rate. For LLM applications, add:
Queue depth stays bounded. If your queue grows without bound under load, you have a capacity planning problem. Assert that queue depth stabilizes rather than growing linearly.
Retry behavior is correct. Count actual retries during the load test. Verify retry rates match expectations — not too many (indicates the rate limit logic is wrong) and not zero (indicates retries aren't happening when they should).
Cost controls activate. If you have per-user or per-request token limits, verify they activate under load. A load test where cost controls never trigger didn't actually test the cost controls.
Error messages are user-friendly. Sample error responses during the load test. Verify they contain user-facing messages, not raw API error objects.
Continuous Behavioral Monitoring During Load
Load tests run periodically. Continuous behavioral monitoring runs constantly.
Set up HelpMeTest to run behavioral tests against your production LLM features every 5 minutes. This catches issues that load tests miss:
- Latency degradation from LLM provider issues
- Quota exhaustion from unexpected traffic spikes
- Response format changes from model updates
- Configuration issues from recent deployments
A behavioral test that verifies your AI feature responds within 30 seconds will fire immediately when a latency spike occurs — before your SLA is breached, before users start complaining.
The Stress Testing Checklist
Before your LLM feature ships:
- Unit tests for transformation logic (parser, formatter, dispatcher)
- Error injection tests for each error type (rate limit, timeout, refusal, malformed output)
- Load test with LLM mock: baseline performance metrics
- Rate limit simulation: user experience during throttling
- Latency spike test: timeout handling
- Context growth test: limits and pruning
- Behavioral tests: user-facing feature works end-to-end
- Continuous monitoring: 5-minute behavioral tests in production
This isn't exhaustive — it's the minimum for a production AI feature. The specific tests you need depend on how your application uses LLMs, what failure modes matter most, and what your users can tolerate.
The goal isn't perfect test coverage. It's catching the class of failures that actually happen: rate limits under load, latency spikes from provider issues, model updates that change output behavior, cost overruns from unexpected usage patterns.
HelpMeTest provides continuous behavioral monitoring for AI features — catch production failures before your users do.