Skip to content

AI API scalability — stress, spike, and soak

LLM and inference APIs fail differently from conventional REST services. GPU memory is finite, worker pools are small, and autoscaling lag can be minutes rather than seconds. A standard stress test exposes these failure modes; a spike test reveals cold-start and autoscaling behaviour; a soak test catches KV-cache memory leaks and slow context-accumulation bugs. This page maps each test type to the AI-specific failure mode it targets.

Failure modeSymptomsTest type that reveals it
GPU saturationp95 latency climbs linearly with VUs; throughput plateausStress
KV-cache exhaustionSudden spike in 500 errors with out of memory bodyStress (high max_tokens)
Worker pool saturationQueue depth grows; latency climbs but RPS stays flatStress
Cold-start lagFirst requests in a spike take 5–30 s while GPU worker initialisesSpike
Autoscaling lagErrors spike during the scale-out window before new instances are readySpike
Rate-limit cascade429 errors cluster at the start of a spikeSpike
Memory leak (KV cache)p95 latency drifts upward over hours; TPOT grows slowlySoak
Connection pool exhaustion503 errors appear only after several hours of sustained loadSoak

Stress test: finding the GPU saturation knee

Section titled “Stress test: finding the GPU saturation knee”

A stress test ramps VUs steadily past the baseline load to find where latency inflects and errors begin.

execution:
- concurrency: 80 # target: 4× baseline load (baseline was 20 VUs)
ramp-up: 15m # slow ramp — 1 VU every ~11 s to observe each step
hold-for: 5m # brief hold at peak
scenario: llm-stress
scenarios:
llm-stress:
requests:
- label: chat-completions
url: https://inference.example.com/v1/chat/completions
method: POST
headers:
Content-Type: application/json
Authorization: "Bearer ${LLM_API_KEY}"
body: >
{
"model": "llama-3-8b-instruct",
"messages": [{"role": "user", "content": "Write a haiku about performance testing."}],
"max_tokens": 64,
"stream": false
}
  1. Duplicate your baseline load test. Rename it llm-stress-80vu. Update the Taurus YAML with concurrency: 80 and ramp-up: 15m.

  2. In the Configuration tab set Virtual users to 80, Ramp-up to 15m, Duration to 20m.

  3. Add a failure criterion: Error rate > 5% → fail — this captures the breaking point automatically.

  4. Click Run. Watch the Overview tab live — the latency chart should show a clear inflection where p95 begins a sustained climb.

  5. When the run ends, note the VU count at the inflection. That is your GPU saturation point for this model and hardware configuration.

  • Latency inflection — the VU count where p95 transitions from flat to a climbing slope. This is the saturation knee: the maximum VUs you can sustain without latency degradation.
  • Error rate step-change — 429s appear first (rate-limit quota), then 503/504s (inference server overwhelmed). A 503 cluster at a specific VU count indicates the worker pool ceiling.
  • Throughput plateau — RPS stops climbing even as VUs increase. This is the inference server’s maximum token-generation throughput. Above this point, adding VUs only increases queue depth.

Spike test: cold-start and autoscaling lag

Section titled “Spike test: cold-start and autoscaling lag”

An inference server that autoscales on GPU demand can take 3–10 minutes to provision and warm a new GPU instance. A spike test with a near-instant VU surge reveals the cold-start window.

import http from 'k6/http';
import { check } from 'k6';
import { Rate } from 'k6/metrics';
const coldStartErrors = new Rate('cold_start_error_rate');
export const options = {
stages: [
{ duration: '2m', target: 5 }, // low baseline (warm state)
{ duration: '30s', target: 60 }, // near-instant surge (spike)
{ duration: '3m', target: 60 }, // hold at spike — autoscaling window
{ duration: '30s', target: 5 }, // drop back
{ duration: '2m', target: 5 }, // recovery observation
],
thresholds: {
http_req_duration: ['p(95)<10000'],
cold_start_error_rate: ['rate<0.10'], // tolerate up to 10% errors during cold-start window
},
};
const ENDPOINT = 'https://inference.example.com/v1/chat/completions';
const API_KEY = __ENV.LLM_API_KEY;
const PAYLOAD = JSON.stringify({
model: 'llama-3-8b-instruct',
messages: [{ role: 'user', content: 'What is 2 + 2?' }],
max_tokens: 32,
stream: false,
});
export default function () {
const res = http.post(ENDPOINT, PAYLOAD, {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`,
},
timeout: '30s',
});
const failed = res.status !== 200;
coldStartErrors.add(failed);
check(res, { 'status 200': (r) => r.status === 200 });
}

The cold_start_error_rate metric lets you distinguish the transient cold-start error window from sustained errors after autoscaling should have completed. If cold_start_error_rate is high throughout the 3-minute hold phase, autoscaling either hasn’t triggered or hasn’t finished provisioning.

Soak test: KV-cache memory leak and connection exhaustion

Section titled “Soak test: KV-cache memory leak and connection exhaustion”

A soak test holds steady load for 4–8 hours to detect slow-growing resource leaks.

What AI soak tests reveal that short tests miss

Section titled “What AI soak tests reveal that short tests miss”
  • KV-cache accumulation: some inference servers improperly evict KV caches, causing memory to grow with each request. p95 TTFT drifts upward as available GPU memory shrinks over hours.
  • TPOT drift: tokens/sec degrades slowly as the model weights compete with growing cached states for GPU VRAM bandwidth.
  • Connection pool exhaustion: HTTP connection pools to the vector DB or embedding service are not properly recycled and exhaust over 4–6 hours.
execution:
- concurrency: 20 # same as baseline load test
ramp-up: 3m
hold-for: 6h # 6-hour soak
scenario: llm-soak
scenarios:
llm-soak:
requests:
- label: chat-completions
url: https://inference.example.com/v1/chat/completions
method: POST
headers:
Content-Type: application/json
Authorization: "Bearer ${LLM_API_KEY}"
body: >
{
"model": "llama-3-8b-instruct",
"messages": [{"role": "user", "content": "Describe a load testing best practice."}],
"max_tokens": 256,
"stream": false
}

Watch for drift rather than peaks:

  • p95 latency trend — flat for 6 hours is the target. Any upward slope (even 100 ms per hour) indicates a resource leak.
  • Throughput decline — if RPS drops at constant VUs, the server is slowing down. Correlate with inference server heap/VRAM metrics.
  • Error rate timing — errors appearing only after 3–4 hours (not at run start) are characteristic of connection pool or file-descriptor exhaustion.
DoDon’t
Run a baseline load test before any stress / spike / soakStress an endpoint before you know its steady-state behaviour
Use short max_tokens for stress tests to see more VU steps before saturationUse large max_tokens for stress — you saturate at too few VUs for a useful curve
Observe the recovery window in spike tests (2 min minimum after VU drop)End the spike run at the VU peak — you miss whether the system recovers
Run soak tests on staging with monitoring enabled for VRAM and heapRun AI soak tests on production shared with real users
Note the exact VU count at the saturation knee for capacity planningReport only peak VU count — the knee is the actionable metric