perf-skills 2.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -57,5 +57,59 @@ curl -X POST "https://neoload-api.tricentis.com/v3/tests/{testId}/start" \
57
57
  - Integrate with **Dynatrace/AppDynamics** via built-in APM connectors.
58
58
  - Use **as-code YAML definitions** (NeoLoad as Code) for version-controlled test configs.
59
59
 
60
+ ---
61
+
62
+ ## NeoLoad as Code (YAML)
63
+
64
+ Version-control your test definitions instead of relying on the GUI `.nlp` file:
65
+
66
+ ```yaml
67
+ # neoload-project.yml
68
+ name: Checkout-Load-Test
69
+ populations:
70
+ - name: Checkout_Users
71
+ user_paths:
72
+ - name: Checkout_Flow
73
+ distribution: 70%
74
+ - name: Browse_Flow
75
+ distribution: 30%
76
+
77
+ scenarios:
78
+ - name: Regression_Load
79
+ populations:
80
+ - name: Checkout_Users
81
+ load_policy:
82
+ steps:
83
+ - type: ramp
84
+ from: 0
85
+ to: 200
86
+ duration: 120s
87
+ - type: constant
88
+ value: 200
89
+ duration: 600s
90
+ - type: ramp
91
+ from: 200
92
+ to: 0
93
+ duration: 60s
94
+
95
+ sla_profiles:
96
+ - name: Checkout_SLA
97
+ thresholds:
98
+ - per_transaction: Checkout
99
+ avg_response_time:
100
+ warn: 500ms
101
+ fail: 1000ms
102
+ error_rate:
103
+ warn: 1%
104
+ fail: 5%
105
+ ```
106
+
107
+ ```bash
108
+ # Validate and run from CLI
109
+ neoload-cli validate neoload-project.yml
110
+ neoload-cli run --scenario Regression_Load
111
+ ```
112
+
60
113
  > For anti-patterns, assertions, think time, and parameterization principles, see **Key Principles** in `SKILL.md`.
61
114
  > For CI/CD integration details, see `../topics/test-execution.md`.
115
+ > For SLO threshold design, see `../topics/slo-capacity.md`.
@@ -404,3 +404,7 @@ If you need help configuring correlation, copy and paste the following prompt te
404
404
  3. Write the exact extraction syntax for the target tool.
405
405
  4. Show the exact injection syntax (including variable syntax, e.g., `${varName}`, `#{varName}`, `{varName}`).
406
406
  5. Explain how to debug and verify that the correlation succeeded.
407
+
408
+ ---
409
+
410
+ > **See also:** Tool-specific extractor syntax in `../tools/jmeter.md`, `../tools/k6.md`, `../tools/gatling.md`, `../tools/locust.md`, `../tools/loadrunner.md`, `../tools/artillery.md`. Script-level correlation patterns in `script-generation.md`.
@@ -212,3 +212,5 @@ WHERE NOT bl.granted;
212
212
  - [ ] Replication lag monitoring active (if using read replicas)
213
213
  - [ ] Connection pool metrics exposed and dashboarded
214
214
  - [ ] Cleanup script ready for test data removal post-test
215
+
216
+ > **See also:** JDBC sampler setup in `../tools/jmeter.md`. Monitoring and dashboards in `observability.md`. Interpreting saturation and bottleneck signals in `results-analysis.md`.
@@ -0,0 +1,179 @@
1
+ # LLM Inference Performance
2
+
3
+ > Scope: load and capacity testing of LLM inference servers (vLLM, TRT-LLM, SGLang, OpenAI-compatible endpoints, Ray Serve LLM, KServe). Covers metric definitions, workload design, tools, and SLO methodology for generative AI.
4
+
5
+ LLM inference is **not conventional HTTP load testing**. Responses stream token-by-token over an open connection, output length is unbounded, and the dominant cost is GPU compute, not network. A generic `k6`/`JMeter` run that only measures request latency will badly misreport performance - it ignores the streaming shape entirely. This topic defines the metrics that actually matter, how to design realistic LLM workloads, and which tools to use.
6
+
7
+ > For general workload theory (Little's Law, open vs closed models), see `workload-design.md`. For analyzing the percentiles and SLO failures this produces, see `results-analysis.md`. For running these tests in CI/CD, see `test-execution.md`.
8
+
9
+ ---
10
+
11
+ ## Why LLM load testing is different
12
+
13
+ | Aspect | Traditional API | LLM inference |
14
+ |---|---|---|
15
+ | Response | Single body, fixed size | Streamed tokens, variable length |
16
+ | Latency signal | TTFB + total | TTFT + inter-token gaps + E2E |
17
+ | Cost driver | CPU/network | GPU memory (KV cache), compute |
18
+ | Throughput unit | Requests/sec | Tokens/sec (not requests/sec) |
19
+ | Concurrency limit | Threads/sockets | KV cache capacity (`max_num_seqs`) |
20
+ | Key risk | Timeouts | Queueing delay, GPU saturation, OOM |
21
+
22
+ Two phases dominate every request:
23
+ - **Prefill** - the model processes the full prompt to build the KV cache. Compute-heavy, determines **TTFT**. Scales with prompt length.
24
+ - **Decode** - tokens generated one at a time using the KV cache. Memory-bandwidth-bound, determines **ITL/TPOT**. Scales with output length.
25
+
26
+ ---
27
+
28
+ ## Core Metrics
29
+
30
+ ### TTFT - Time to First Token
31
+ Time from request send to first streamed token. Driven by prompt length, queue/self.time, and prefill speed. The primary *perceived responsiveness* metric for chat/coding assistants. High TTFT under load usually means the scheduler is queuing requests (KV cache exhausted) rather than a slow model.
32
+
33
+ ### TPOT / ITL - Generation smoothness
34
+ - **TPOT (Time Per Output Token)** - average gap between tokens for a single request: `(E2E - TTFT) / (output_tokens - 1)`. Report the mean of per-request TPOTs.
35
+ - **ITL (Inter-Token Latency)** - same gaps, but token-weighted across all requests (mean of every gap). This is the system's steady streaming speed.
36
+ - **Which to use**: TPOT compares per-request behavior (each request equal); ITL estimates system-wide streaming feel across mixed traffic. A bursty ITL (high p99 vs mean) makes output appear in clumps - the classic "GPU contention" signature.
37
+
38
+ ### E2E Latency
39
+ Total time from prompt to final token. `E2E = TTFT + (output_tokens - 1) * TPOT`. Matters for batch/codegen/summarization where the full response is needed before the next step.
40
+
41
+ ### TPS - Token Throughput
42
+ - **System TPS** - total output tokens/sec across all requests. Raw capacity; rises with load until GPU-saturated. `TPS = output_tokens / (T_last - T_first)`.
43
+ - **User TPS** - tokens/sec a single user experiences; `≈ 1 / ITL` at long outputs. Drops as concurrency rises because the engine shares the GPU.
44
+
45
+ ### RPS - Request Throughput
46
+ Completed requests/sec. Dominant metric for high-volume short-prompt traffic (chatbots, search, API gateways). Shorter prompts + higher `max_num_seqs` raise RPS.
47
+
48
+ ### Goodput (the metric that matters most)
49
+ `Goodput = (requests meeting ALL SLOs) / total_requests * 100%`. Unlike TPS/RPS, goodput tells you what fraction of users got an *acceptable* experience. A system can show high TPS while most requests violate latency SLOs. Always define goodput with explicit thresholds, e.g. TTFT < 500ms, TPOT < 15ms, E2E < 2s. As load climbs, goodput falls even as raw throughput keeps rising - that crossover is your real capacity limit.
50
+
51
+ ### Percentiles
52
+ Report **p50/p95/p99** for TTFT, ITL, and E2E - never just averages. Averages hide the unlucky 5% whose tokens arrive in bursts. p99 is the near-worst-case; if p99 meets SLO, the system is consistent.
53
+
54
+ ---
55
+
56
+ ## Workload Dimensions
57
+
58
+ Controlling these is what separates a useful LLM test from a misleading one:
59
+
60
+ | Dimension | Effect | How to set |
61
+ |---|---|---|
62
+ | **Prompt length (ISL)** | Longer → higher TTFT, more KV cache | Use real traffic (ShareGPT) or synthetic ranges |
63
+ | **Output length (OSL)** | Longer → higher E2E, more decode cost | Real distributions; `--random-output-len` for synthetic |
64
+ | **Concurrency** | Bounded by KV cache, not sockets | Set `--max-concurrency` to simulate gateway limits |
65
+ | **Request rate** | Open-model arrival (Poisson) | `--request-rate`; `inf` for max throughput |
66
+ | **Burstiness** | Gamma-distributed arrivals | `1.0` realistic, `0.1-0.5` stress, `2-5` uniform |
67
+ | **Batching (`max_num_seqs`)** | Higher → more RPS, worse per-user latency | Tune on the server, not the client |
68
+
69
+ **KV cache math**: `max_concurrency ≈ KV_cache_tokens / max_model_len`. vLLM prints this at startup. Set test concurrency to 80-90% of it for capacity planning; use the full value as the SLA limit.
70
+
71
+ ---
72
+
73
+ ## Tools
74
+
75
+ Purpose-built LLM benchmarkers surface token metrics natively. Prefer them over generic HTTP load testers.
76
+
77
+ | Tool | Best for | Notes |
78
+ |---|---|---|
79
+ | **vLLM `bench serve`** | Serving benchmarks against an OpenAI-compatible endpoint | Native TTFT/TPOT/ITL/TPS, ShareGPT + synthetic datasets, ramp-up, goodput via percentiles |
80
+ | **GuideLLM** | Production vLLM SLA/capacity studies | Auto reports, live progress, profile-based; recommended by vLLM for production |
81
+ | **NVIDIA GenAI-Perf** | Multi-backend (TRT-LLM, Triton, vLLM), concurrency/rate modes | Emits TTFT, ITL, output TPS, goodput; pairs with Perf Analyzer |
82
+ | **llmperf / LLM-Perf** | Quick pointwise latency/throughput checks | Lightweight, great for smoke and regression |
83
+ | **LLM Locust** | Distributed load on Locust, GenAI metrics | Use when you already run Locust fleets |
84
+ | **k6 + custom metrics** | Unified CI with existing k6 stacks | Must instrument streaming manually: capture TTFT from first SSE chunk, ITL between chunks, count tokens. See `../tools/k6.md` for metric primitives |
85
+
86
+ > **Anti-pattern (per current research):** do not misuse model-server micro-benchmarkers (vLLM `bench`, SGLang bench) as production-level evaluators. They optimize for regression/feature testing, not realistic arrival patterns. Use them for component baselines; use GuideLLM/GenAI-Perf/production-style load for capacity and SLO validation.
87
+
88
+ ---
89
+
90
+ ## Quick Start Commands
91
+
92
+ ```bash
93
+ # vLLM bench serve - max throughput probe with ShareGPT data
94
+ python -m vllm.entrypoints.openai.api_server --model meta-llama/Llama-3-8B &
95
+ python -m vllm bench serve \
96
+ --model meta-llama/Llama-3-8B \
97
+ --endpoint http://localhost:8000/v1/completions \
98
+ --dataset-name sharegpt \
99
+ --request-rate inf \
100
+ --max-concurrency 256 \
101
+ --num-prompts 500
102
+
103
+ # vLLM bench serve - realistic arrival pattern
104
+ python -m vllm bench serve \
105
+ --model meta-llama/Llama-3-8B \
106
+ --endpoint http://localhost:8000/v1/completions \
107
+ --dataset-name sharegpt \
108
+ --request-rate 10 \
109
+ --burstiness 1.0 \
110
+ --num-prompts 200
111
+
112
+ # GuideLLM - production SLA study
113
+ pip install guidedllm
114
+ guidedllm evaluate \
115
+ --model meta-llama/Llama-3-8B \
116
+ --base-url http://localhost:8000/v1 \
117
+ --data emulated --rate 10 --duration 120
118
+
119
+ # NVIDIA GenAI-Perf - multi-backend benchmark
120
+ genai-perf profile \
121
+ --model meta-llama/Llama-3-8B \
122
+ --endpoint-type chat \
123
+ --url localhost:8000 \
124
+ --concurrency 64 \
125
+ --request-count 200
126
+
127
+ # llmperf - quick latency/throughput check
128
+ python token_benchmark_ray.py \
129
+ --model meta-llama/Llama-3-8B \
130
+ --mean-input-tokens 512 \
131
+ --mean-output-tokens 128 \
132
+ --num-concurrent-requests 32 \
133
+ --results-dir ./results
134
+ ```
135
+
136
+ ---
137
+
138
+ ## Test Design & Methodology
139
+
140
+ 1. **Start from real traffic shapes.** Use ShareGPT or captured production traces for prompt/output length distributions. Synthetic `random` datasets are fine for stress but unrealistic for sizing.
141
+ 2. **Define SLOs as goodput thresholds first.** e.g. "TTFT p95 < 500ms, ITL p95 < 20ms, E2E p95 < 3s" → goodput target 99%.
142
+ 3. **Run a max-throughput probe** (`--request-rate inf --max-concurrency <limit>`) to find the concurrency ceiling and baseline TPS.
143
+ 4. **Sweep concurrency / request rate** to find the goodput cliff - the load where SLO compliance drops. That is capacity.
144
+ 5. **Test ramp-up and spikes** (linear/exponential ramp, bursty arrival) to validate autoscaling and queue behavior.
145
+ 6. **Always assert on token metrics, not just HTTP 200.** A 200 with a 10s TTFT is a failed request.
146
+
147
+ ### Workload pattern recipes (vLLM bench semantics)
148
+ | Goal | `--request-rate` | `--burstiness` | `--max-concurrency` |
149
+ |---|---|---|---|
150
+ | Max throughput | `inf` | n/a | limited |
151
+ | Realistic baseline | 5-20 | 1.0 | inf |
152
+ | Stress / resilience | 20-100 | 0.1-0.5 | inf |
153
+ | Latency profiling | 1-10 | 2-5 | inf |
154
+ | Capacity / SLA | target rate | 1.0 | SLA limit |
155
+
156
+ ---
157
+
158
+ ## Common Mistakes
159
+
160
+ - **Measuring only HTTP latency** - TTFT and streaming gaps are invisible to `http_req_duration`. You'll report a green test while users stare at a blank cursor.
161
+ - **Ignoring output length variance** - fixed `max_tokens` hides the real E2E spread; use realistic OSL distributions.
162
+ - **Unbounded concurrency** - without `--max-concurrency`, the client fires until the GPU chokes; you measure collapse, not capacity. The KV cache, not sockets, is the real cap.
163
+ - **Confusing TPS with user experience** - high system TPS can coexist with terrible per-user ITL. Track both; report goodput.
164
+ - **Averaging instead of percentiles** - a 200ms mean TTFT with 8s p99 is a broken chat UX.
165
+ - **Using a micro-benchmarker for production SLOs** - `vllm bench`/SGLang bench validate the engine, not your serving capacity under real arrival patterns.
166
+ - **No streaming instrumentation in k6** - a naive `http.get` to a streaming endpoint counts only the final byte; you must parse SSE chunks to get TTFT/ITL.
167
+ - **Forgetting prompt caching** - prefix caching dramatically cuts TTFT for repeated prefixes; test with and without it (e.g. RAG, system prompts) to size the win.
168
+
169
+ ---
170
+
171
+ ## Observability
172
+
173
+ Track these server-side alongside the client metrics above:
174
+ - **KV cache utilization** - the true saturation signal; when it pins at 100%, TTFT inflates via queueing.
175
+ - **Batch size / `num_running` vs `num_waiting`** - waiting > 0 means you're over concurrency.
176
+ - **Prefill vs decode time split** - isolates whether slowness is prompt-side or generation-side.
177
+ - **GPU util + memory** - confirms you're compute-bound, not starved.
178
+
179
+ > For dashboards/APM integration patterns, see `observability.md`. For interpreting the goodput cliff and saturation, see `results-analysis.md`.
@@ -312,3 +312,5 @@ Define performance budgets in CI to prevent regressions:
312
312
  - [ ] Concurrency limits and provisioned concurrency validated
313
313
  - [ ] Frontend Core Web Vitals measured under backend load
314
314
  - [ ] Performance budgets defined and enforced in CI
315
+
316
+ > **See also:** CI/CD pipeline patterns in `test-execution.md`. Kubernetes and mesh monitoring in `observability.md`. SLO headroom for autoscaling decisions in `slo-capacity.md`.
@@ -199,3 +199,5 @@ Synthetic monitors catch regressions that slip through staging without requiring
199
199
  - [ ] Log aggregation active and searchable
200
200
  - [ ] Baseline metrics screenshot taken before test starts (for comparison)
201
201
  - [ ] Metrics retention configured to keep results for trend analysis
202
+
203
+ > **See also:** k6 metric primitives in `../tools/k6.md`. JMeter Backend Listener in `../tools/jmeter.md`. Interpreting the metrics in `results-analysis.md`. SLO thresholds in `slo-capacity.md`.
@@ -212,3 +212,5 @@ If something breaks during a test:
212
212
  4. **Roll back if needed** - restore previous version, restart services.
213
213
  5. **Preserve evidence** - don't restart services without capturing logs and heap dumps.
214
214
  6. **Post-mortem** - document what broke, at what load, what the root cause was.
215
+
216
+ > **See also:** CI/CD execution patterns in `test-execution.md`. Monitoring during production tests in `observability.md`. SLO and error budget framing in `slo-capacity.md`.
@@ -58,6 +58,36 @@ export default function () {
58
58
  - **Connection reuse**: Keep connections open across iterations; don't connect/close per request.
59
59
  - **Streaming throughput**: For server-streaming RPCs, measure messages-per-second, not just request latency.
60
60
  - **Deadline propagation**: Set gRPC deadlines in tests to match production timeouts.
61
+
62
+ ### Gatling gRPC Example
63
+
64
+ ```scala
65
+ // Requires: gatling-grpc plugin (io.gatling:gatling-grpc)
66
+ import io.gatling.core.Predef._
67
+ import io.gatling.grpc.Predef._
68
+ import io.grpc.ManagedChannelBuilder
69
+ import scala.concurrent.duration._
70
+
71
+ class GrpcSimulation extends Simulation {
72
+ val channel = ManagedChannelBuilder
73
+ .forAddress("grpc-server", 50051)
74
+ .usePlaintext()
75
+ .build()
76
+
77
+ val scn = scenario("gRPC Unary")
78
+ .exec(
79
+ grpc("SayHello")
80
+ .rpc(HelloServiceGrpc.METHOD_SAY_HELLO)
81
+ .payload(HelloRequest(greeting = "perf-test"))
82
+ .check(statusCode.is(StatusCode.OK))
83
+ )
84
+ .pause(1)
85
+
86
+ setUp(
87
+ scn.injectOpen(rampUsers(100).during(30.seconds))
88
+ ).protocols(grpc(channel))
89
+ }
90
+ ```
61
91
  - **Load balancer awareness**: gRPC over HTTP/2 with persistent connections can cause uneven load across backends - test with client-side load balancing or L7 proxy.
62
92
  - **Protobuf payload size**: Binary encoding is smaller than JSON - adjust throughput expectations accordingly.
63
93
 
@@ -199,6 +229,39 @@ export default function () {
199
229
  3. **Measure server-side**: File descriptor count, memory per connection, event loop lag.
200
230
  4. **Test reconnection storms**: Kill server, observe client reconnection behavior and server recovery.
201
231
 
232
+ ### Gatling WebSocket Example
233
+
234
+ ```scala
235
+ import io.gatling.core.Predef._
236
+ import io.gatling.http.Predef._
237
+ import scala.concurrent.duration._
238
+
239
+ class WebSocketSimulation extends Simulation {
240
+ val httpProtocol = http.baseUrl("https://app.example.com")
241
+
242
+ val scn = scenario("WebSocket Chat")
243
+ .exec(
244
+ ws("Connect WS")
245
+ .connect("/ws/chat")
246
+ .header("Authorization", "Bearer #{token}")
247
+ )
248
+ .exec(
249
+ ws("Subscribe")
250
+ .sendText("""{"type":"subscribe","channel":"updates"}""")
251
+ .await(30.seconds)(
252
+ ws.checkTextMessage("subscribed")
253
+ .check(bodyJsonPath("$.type").is("subscribed"))
254
+ )
255
+ )
256
+ .pause(30) // hold connection open
257
+ .exec(ws("Close").close)
258
+
259
+ setUp(
260
+ scn.injectOpen(rampUsers(500).during(60.seconds))
261
+ ).protocols(httpProtocol)
262
+ }
263
+ ```
264
+
202
265
  ---
203
266
 
204
267
  ## Message Queue / Event Streaming Testing
@@ -272,3 +335,5 @@ export function teardown() {
272
335
  - [ ] Backpressure / flow control behavior validated
273
336
  - [ ] Error codes and retry behavior tested (gRPC status codes, AMQP nacks, etc.)
274
337
  - [ ] End-to-end latency measured (not just request latency)
338
+
339
+ > **See also:** k6 scripting (all examples above) in `../tools/k6.md`. Gatling gRPC/WebSocket in `../tools/gatling.md`. JMeter JMS/plugins in `../tools/jmeter.md`. Workload design for streaming protocols in `workload-design.md`.
@@ -242,3 +242,5 @@ Structure your report:
242
242
  - Error log samples
243
243
  - Environment configuration
244
244
  ```
245
+
246
+ > **See also:** Metric collection and dashboards in `observability.md`. SLO interpretation and error budgets in `slo-capacity.md`. LLM-specific metrics (TTFT, TPOT, goodput) in `llm-inference.md`.
@@ -39,26 +39,17 @@ Record a HAR file from the browser → import into your tool → clean up and pa
39
39
 
40
40
  ---
41
41
 
42
- ## Correlation Deep Dive
42
+ ## Correlation
43
43
 
44
44
  Correlation is extracting a dynamic value from a response and using it in a subsequent request. Missing correlation is the #1 cause of performance script failure.
45
45
 
46
- ### Values that always need correlation
47
- - **Session tokens** (JSESSIONID, ASP.NET_SessionId)
48
- - **Auth tokens** (JWT, OAuth access_token, refresh_token)
49
- - **CSRF tokens** (`_token`, `authenticity_token`, `__RequestVerificationToken`)
50
- - **View state** (ASP.NET `__VIEWSTATE`, `__EVENTVALIDATION`)
51
- - **Resource IDs** (created order ID, cart ID, uploaded file ID)
52
- - **One-time codes** (OTP, nonce, challenge)
53
- - **Timestamps/signatures** used in request signing
54
-
55
- ### Correlation debugging approach
56
- 1. Run the script with 1 VU.
57
- 2. Look for HTTP 4xx errors (especially 403 Forbidden, 422 Unprocessable Entity).
58
- 3. Check the request body/headers - is a token missing or stale?
59
- 4. Use the tool's debug output or proxy (Fiddler, Charles) to inspect live traffic.
60
- 5. Identify where the value appears in a *previous* response.
61
- 6. Add an extractor at that response, reference the variable in the failing request.
46
+ > **Full reference:** see `correlation.md` for the complete correlation workflow, tool-specific extractor syntax, tech-stack rules (ASP.NET, Java, SAP, OAuth), brittle-vs-robust regex guidance, and the diagnostic playbook.
47
+
48
+ ### Quick checklist
49
+ - Session tokens, CSRF tokens, OAuth codes, resource IDs, and one-time nonces must always be correlated.
50
+ - Run with 1 VU first; 403/422 errors usually mean a missing or stale token.
51
+ - Use a proxy (Fiddler, Charles, mitmproxy) to find where the value appears in a previous response.
52
+ - Add the extractor at that response, reference the variable in the failing request.
62
53
 
63
54
  ---
64
55
 
@@ -219,3 +210,5 @@ Treat performance scripts as production code:
219
210
  - [ ] Single VU smoke test passes cleanly
220
211
  - [ ] Script loads test data from external source (CSV, API)
221
212
  - [ ] Environment-specific values in config/env vars
213
+
214
+ > **See also:** Full correlation reference in `correlation.md`. Parameterization and data feeds in `test-data.md`. Tool-specific syntax in `../tools/k6.md`, `../tools/jmeter.md`, `../tools/gatling.md`, `../tools/locust.md`.
@@ -0,0 +1,136 @@
1
+ # SLO & Capacity Planning
2
+
3
+ > Scope: turning load-test results into service level objectives, error budgets, and capacity decisions. Complements `workload-design.md` (designing the load) and `results-analysis.md` (reading the numbers). Read those first if you have not designed or run the test yet.
4
+
5
+ This topic covers the *decision* layer of performance engineering: how to define SLIs/SLOs that mean something, how to convert a load-test curve into a capacity number and a headroom plan, and how to enforce all of it as an automated CI gate.
6
+
7
+ > For LLM-specific SLOs (TTFT, goodput), see `llm-inference.md`. For the tool syntax that enforces SLOs (`ensure`, `thresholds`), see the relevant tool file.
8
+
9
+ ---
10
+
11
+ ## SLI / SLO / Error Budget
12
+
13
+ ### Definitions
14
+ - **SLI (Service Level Indicator)** - the actual measured signal, e.g. request p95 latency, error rate, availability.
15
+ - **SLO (Service Level Objective)** - the target you commit to for an SLI over a window, e.g. "p95 < 500ms for 99% of requests over 28 days."
16
+ - **Error Budget** - `1 - SLO`. A 99% SLO means a 1% error budget: you may violate the objective 1% of the time before it's a breach. Budgets make trade-offs explicit (ship features vs. burn reliability).
17
+
18
+ ### Choosing SLIs
19
+ Pick SLIs users actually feel, not ones that are easy to measure:
20
+
21
+ | User-facing concern | SLI | Typical SLO |
22
+ |---|---|---|
23
+ | Responsiveness | Request p95 latency | < 500ms |
24
+ | Worst-case tail | Request p99 latency | < 1,500ms |
25
+ | Reliability | Error rate (5xx + timeouts) | < 1% (99% success) |
26
+ | Availability | Successfully served requests / total | 99.9% |
27
+ | Throughput | Sustained RPS at SLO latency | ≥ peak + headroom |
28
+ | Freshness (data) | Staleness of served data | < 60s |
29
+
30
+ For streaming/LLM, use token-aware SLIs (TTFT, ITL) and **goodput** (fraction of requests meeting all thresholds) instead of raw latency - see `llm-inference.md`.
31
+
32
+ ### Windowing
33
+ - Use a **rolling window** (e.g. 28 days) so a single bad day does not trigger an alert; it consumes budget gradually.
34
+ - Report SLO attainment as `good_events / total_events` over the window. If attainment < SLO, the budget is exhausted.
35
+ - Set **alerts on burn rate**, not on instantaneous SLO pass/fail. Fast burn (e.g. 14× rate) means a major incident; slow burn means a trend to watch.
36
+
37
+ ### Multi-window burn-rate alerting (pattern)
38
+ Define fast and slow burn signals so you catch both outages and slow erosion:
39
+ ```
40
+ Fast burn: burn rate ≥ 14 over 1h → page
41
+ Slow burn: burn rate ≥ 2 over 6h → ticket
42
+ ```
43
+ A burn rate of `X` means the budget is being consumed `X` times faster than the 28-day baseline allows.
44
+
45
+ ---
46
+
47
+ ## From Load Test to Capacity
48
+
49
+ A load test produces a curve of latency/error vs load. Capacity is the point on that curve where SLOs still hold, **plus** the headroom you keep for safety.
50
+
51
+ ### The saturation curve
52
+ ```
53
+ latency
54
+ │ ╱‾‾‾‾‾‾‾ ← cliff: SLO breach
55
+ │ ─────────────
56
+ └──────────────────────── load (RPS/VUs)
57
+ [safe zone] [capacity point]
58
+ ```
59
+ - **Capacity point** = highest sustained load at which all SLOs (p95, error rate, goodput) still pass.
60
+ - **Saturation point** = where throughput flatlines and latency explodes (the "cliff" in `results-analysis.md`).
61
+ - **Never plan to capacity.** Plan to capacity minus headroom.
62
+
63
+ ### Headroom rule
64
+ - Target **peak production load at ~60-70% of measured capacity** under normal operation.
65
+ - Keep **~30-40% headroom** to absorb traffic spikes, node failures, and deployment blips without breaching SLO.
66
+ - For autoscaling systems, headroom is what lets new replicas come up before the SLO burns.
67
+
68
+ ### Inverting Little's Law to size capacity
69
+ Given a target arrival rate and response time, concurrency is `N = λ × W` (see `workload-design.md`). Use it both ways:
70
+ - **To size load**: known λ and W → required VUs (already in workload-design).
71
+ - **To size infrastructure**: if the test shows a single replica sustains `C` RPS at SLO, replicas needed = `target_RPS / (C × utilization_target)`. With a 70% utilization target and 1,000 target RPS at 200 RPS/replica → `1000 / (200 × 0.7)` ≈ **8 replicas** (round up, add 1 for failure tolerance).
72
+
73
+ ### Capacity from the goodput cliff
74
+ For LLM and streaming services, capacity = load at the goodput cliff (see `llm-inference.md`). Report "sustains 200 concurrent users at 99% goodput; goodput drops to 80% at 350" - that 200 is your planning number, 350 is your hard ceiling.
75
+
76
+ ---
77
+
78
+ ## Worked Example
79
+
80
+ **Inputs:** Peak prod = 800 RPS. Test shows p95 < 500ms and error < 1% hold up to 1,200 RPS on the current 6-replica cluster; saturation at ~1,500 RPS.
81
+
82
+ | Decision | Calculation | Result |
83
+ |---|---|---|
84
+ | Capacity point | measured SLO-holding load | 1,200 RPS |
85
+ | Planning target | 60-70% of capacity | 720-840 RPS |
86
+ | Headroom | 1,200 - 800 peak | 33% (acceptable) |
87
+ | Replicas at growth | if peak grows to 1,500 RPS: `1500 / (200 × 0.7)` | 11 replicas (was 6) |
88
+ | Alert threshold | 80% of capacity as early warning | alert at ~960 RPS |
89
+
90
+ ---
91
+
92
+ ## CI Regression Gating
93
+
94
+ SLOs are useless if they are only in a report. Enforce them automatically:
95
+
96
+ 1. **Gate on thresholds, not on "it ran."** Every tool has an SLO mechanism - k6 `thresholds`, Artillery `ensure`, Gatling Enterprise assertions, JMeter exit codes. A run with no gate always passes CI (see each tool file's Common Mistakes).
97
+ 2. **Compare against baseline, not just absolute.** A p95 of 480ms passes a 500ms SLO but is a +60% regression vs last week's 300ms. Track percentile deltas build-over-build.
98
+ 3. **Set a regression threshold.** e.g. "fail the build if p95 rises > 10% vs baseline, or error rate > 0.5%." Avoid zero-tolerance (noise); avoid loose (misses real regressions).
99
+ 4. **Require statistical significance for soak/large runs.** A 1-run blip should not fail CI; use multiple iterations or a confidence band.
100
+ 5. **Publish the SLO report in the pipeline.** Surface p50/p95/p99, throughput, error rate, and budget consumption on every run so regressions are visible, not buried.
101
+
102
+ > For the per-tool gate syntax, see `../tools/k6.md` (`thresholds`), `../tools/artillery.md` (`ensure`), `../tools/gatling.md`, `../tools/jmeter.md`. For running these in pipelines, see `test-execution.md`.
103
+
104
+ ---
105
+
106
+ ## Common Mistakes
107
+
108
+ - **Averaging for SLOs** - an average of 200ms can hide a p99 of 5s. SLOs must be percentile-based.
109
+ - **SLO with no error budget** - a target with no burn policy becomes a meaningless number; you can never "spend" reliability intentionally.
110
+ - **Planning to the saturation point** - sizing for max measured throughput leaves zero margin; the first spike breaches SLO.
111
+ - **Confusing capacity with peak** - if peak ≈ capacity, you have no headroom. Target 60-70%.
112
+ - **One-shot capacity number** - capacity drifts as code, data, and dependencies change. Re-baseline on major releases.
113
+ - **Gating CI on run success only** - "test passed" ≠ "SLO met." Without a threshold/ensure gate, regressions ship green.
114
+ - **Ignoring tail latency in capacity** - a system can hold p95 at capacity while p99 is 10× worse; gate on the tail users actually feel.
115
+ - **No burn-rate alerting** - alerting on instantaneous SLO state misses slow erosion until the budget is already gone.
116
+
117
+ ---
118
+
119
+ ## Reporting SLO & Capacity
120
+
121
+ Structure the decision section for stakeholders:
122
+
123
+ ```
124
+ 1. SLO SUMMARY
125
+ - p95 < 500ms: 99.2% attainment (budget: 1% used 0.8%)
126
+ - Error rate < 1%: 99.97% attainment
127
+ 2. CAPACITY
128
+ - Measured capacity: 1,200 RPS @ SLO
129
+ - Current peak: 800 RPS (33% headroom)
130
+ - Saturation: ~1,500 RPS
131
+ 3. RECOMMENDATION
132
+ - Scale to 8 replicas before peak season (growth to 1,500 RPS)
133
+ - Alert at 960 RPS (80% of capacity)
134
+ 4. REGRESSION GATE
135
+ - p95 > +10% vs baseline fails CI
136
+ ```
@@ -194,3 +194,5 @@ const user = users[__VU - 1]; // VU 1 gets row 0, VU 2 gets row 1, etc.
194
194
  - [ ] CSV files available on all injector nodes (distributed tests)
195
195
  - [ ] Sensitive data masked/anonymized (not using real PII in test data)
196
196
  - [ ] Date/time sensitive records accounted for (e.g., expiry dates set far in future)
197
+
198
+ > **See also:** Tool-specific parameterization syntax in `../tools/k6.md` (SharedArray), `../tools/jmeter.md` (CSV Data Set Config), `../tools/gatling.md` (feeders), `../tools/locust.md`. Distributing data to injectors in `test-execution.md`.
@@ -114,12 +114,37 @@ bzt tests/load-test.jmx \
114
114
  ```
115
115
 
116
116
  ### AWS (DIY Cloud Execution)
117
- ```bash
118
- # Spin up EC2 injectors via Terraform, then run JMeter/k6
119
- # Use Auto Scaling Groups for burst capacity
120
- # Use S3 to store results, CloudWatch for metrics
121
117
 
122
- # Cost optimization: use Spot instances for injectors (price vs reliability tradeoff)
118
+ ```bash
119
+ # 1. Launch injectors with AWS CLI (or Terraform)
120
+ aws ec2 run-instances \
121
+ --image-id ami-0abcdef1234567890 \
122
+ --instance-type c5.2xlarge \
123
+ --count 4 \
124
+ --spot-price "0.15" \
125
+ --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=load-injector}]'
126
+
127
+ # 2. Install k6 on each injector (via user-data or SSM)
128
+ aws ssm send-command \
129
+ --instance-ids i-0abc i-0def i-0ghi i-0jkl \
130
+ --document-name "AWS-RunShellScript" \
131
+ --parameters 'commands=["sudo apt-get install -y k6"]'
132
+
133
+ # 3. Distribute the test script and run in parallel
134
+ for ip in 10.0.1.{10..13}; do
135
+ scp script.js ec2-user@$ip:~/ && \
136
+ ssh ec2-user@$ip "k6 run --vus 250 --duration 10m script.js --out json=/tmp/results.json" &
137
+ done
138
+ wait
139
+
140
+ # 4. Collect results to S3
141
+ for ip in 10.0.1.{10..13}; do
142
+ scp ec2-user@$ip:/tmp/results.json ./results/$(echo $ip | tr '.' '-')-results.json
143
+ done
144
+ aws s3 cp ./results/ s3://my-load-test-bucket/runs/$(date +%Y%m%d)/ --recursive
145
+
146
+ # Cost optimization: use Spot instances for injectors (60-90% cheaper).
147
+ # Handle interruptions with a 2-min warning hook that flushes partial results.
123
148
  ```
124
149
 
125
150
  ---
@@ -279,3 +304,5 @@ Always monitor the injector resources alongside the SUT:
279
304
  - [ ] Smoke test (1 VU) passed before full run
280
305
  - [ ] Team notified of test window
281
306
  - [ ] Rollback plan in place (especially for production testing)
307
+
308
+ > **See also:** Tool-specific CLI and distributed setup in `../tools/jmeter.md`, `../tools/k6.md`, `../tools/locust.md`, `../tools/gatling.md`, `../tools/artillery.md`. Monitoring during execution in `observability.md`. Production safety controls in `production-testing.md`.