Performance Testing
What it is
Section titled “What it is”Performance testing measures how a system behaves under load: how fast it responds, how much throughput it sustains, and where it breaks. It is not one test but a family of profiles.
VUs 50 │ ┌──────────────┐ load / soak │ / \ 0 └─────┘ └──────► time
stress: ramp until it breaks spike: sudden jump soak: long steady runThe numbers that matter are latency percentiles (p50, p95, p99 — never just the average), throughput (requests per second), and error rate at each load step.
When to use it / when to avoid it
Section titled “When to use it / when to avoid it”Use it for
- Before a launch, sale or migration where traffic will step up sharply
- Verifying an SLA or service-level objective (e.g. p95 < 300 ms at 1k rps)
- Finding the saturation point and the first bottleneck (CPU, pool, lock, GC)
- Checking memory or connection leaks with a sustained soak run
Avoid or reduce it when
- The feature is still changing shape — optimise after the design settles
- The environment is not representative (shared CI runner, laptop, cold cache)
- You only read the average latency; percentiles are where the pain hides
- You run a full soak on every commit — keep a fast smoke load in CI instead
Frameworks & tools
Section titled “Frameworks & tools”| Tool | Ecosystem | Notes |
|---|---|---|
| k6 | JS/TS | Scriptable in JavaScript, CLI-first, great CI story |
| Apache JMeter | Java / XML | GUI + CLI, huge protocol coverage, mature |
| Gatling | Java / Scala / Kotlin | DSL, excellent HTML reports |
| Locust | Python | Plain Python load scripts, distributed workers |
| vegeta | Go | Tiny CLI, constant-rate attack |
| NBomber | C# / .NET | Fluent .NET load tests |
| Artillery | Node.js | YAML scenarios, serverless-friendly |
| Grafana Cloud k6 | SaaS | Hosted runs, dashboards |
Quick setup
Section titled “Quick setup”brew install k6 # or: docker pull grafana/k6k6 run script.jspip install locustlocust -f locustfile.py --headless -u 100 -r 10 -t 2mgo install github.com/tsenart/vegeta@latestecho "GET http://localhost:8080/" | vegeta attack -rate=200 -duration=30s | vegeta report<!-- Maven: Gatling --><dependency> <groupId>io.gatling.highcharts</groupId> <artifactId>gatling-charts-highcharts</artifactId> <version>3.11.5</version> <scope>test</scope></dependency>dotnet add package NBomberExample
Section titled “Example”import http from 'k6/http';import { check, sleep } from 'k6';
export const options = { stages: [ { duration: '30s', target: 50 }, // ramp up { duration: '1m', target: 50 }, // steady { duration: '30s', target: 0 }, // ramp down ], thresholds: { http_req_duration: ['p(95)<300'], http_req_failed: ['rate<0.01'], },};
export default function () { const res = http.get('http://localhost:8080/health'); check(res, { 'status is 200': (r) => r.status === 200 }); sleep(1);}from locust import HttpUser, task, between
class ApiUser(HttpUser): wait_time = between(0.5, 1.5) host = "http://localhost:8080"
@task def health(self): self.client.get("/health")echo "GET http://localhost:8080/health" | \ vegeta attack -rate=200/s -duration=45s | vegeta report -type=textimport io.gatling.core.Predef._import io.gatling.http.Predef._
class HealthSimulation extends Simulation { val httpProtocol = http.baseUrl("http://localhost:8080") val scn = scenario("health").exec(http("/health").get("/health")) setUp(scn.inject(rampUsers(50).during(30))).protocols(httpProtocol)}using NBomber.CSharp;using NBomber.Http.CSharp;
var scenario = Scenario.Create("health", async context =>{ using var http = new HttpClient(); var response = await http.GetAsync("http://localhost:8080/health"); return response.IsSuccessStatusCode ? Response.Ok() : Response.Fail();}).WithLoadSimulations(Simulation.KeepConstant(copies: 50, during: TimeSpan.FromMinutes(1)));
NBomberRunner.RegisterScenarios(scenario).Run();Anti-patterns & best practices
Section titled “Anti-patterns & best practices”- Report percentiles, not averages. Averages hide the slow tail that real users feel.
- Match the environment. Test where the shape of the data, network and instance sizes resemble production, or the numbers mean nothing.
- Fix one variable at a time. Change load, not also the build, so you can attribute the result.
- Set thresholds in code. Fail the pipeline on
p95or error-rate regressions, not on a human eyeballing a graph. - Model think time. No pauses measures raw throughput, not user behaviour.
- Watch the system, not only the client. Correlate load with CPU, memory, GC, pool saturation and downstream latency.
Self-assessment quiz
Section titled “Self-assessment quiz”Self-assessment quiz
Pick one answer per question, then press Check answers.