Load Testing Production Systems: k6, Locust, Stress Testing, Soak Testing, and Reporting

Published 18 min read
load-testingk6locustperformancestress-testingmonitoring

Load testing is not optional for production systems. Without it, you discover your capacity limits when real users hit them. This guide covers setting up k6 and Locust, designing test scenarios for different failure modes, establishing baseline metrics, running stress, soak, and spike tests safely against production, and building automated performance reports.

According to PagerDuty's 2026 incident analysis, 34% of production outages are capacity-related and preventable with load testing. Teams that run weekly load tests experience 67% fewer performance-related incidents. The average cost of a performance-related outage for a SaaS company is $5,600 per minute.

---

Test Types and When to Use Each

Test TypePurposeDurationLoad PatternWhen to Run

SmokeVerify test scripts work1-2 min1-5 usersEvery CI/CD run
LoadValidate expected traffic10-15 minNormal trafficWeekly
StressFind breaking point15-30 minRamp beyond capacityBefore launches
SoakFind memory leaks2-8 hoursSustained normal loadMonthly
SpikeTest sudden traffic bursts5-10 minSudden 10x spikeBefore marketing campaigns
BreakpointFind absolute maximum15-30 minContinuous rampQuarterly

---

Setting Up k6

k6 is a developer-centric load testing tool that uses JavaScript for test scripts.

<h1>Install k6</h1>

<h1>macOS</h1>

brew install k6

<h1>Linux (Debian/Ubuntu)</h1>

sudo gpg -k

sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg \

--keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D68

echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" \

| sudo tee /etc/apt/sources.list.d/k6.list

sudo apt-get update && sudo apt-get install k6

k6 Load Test Script

// tests/load/api-load-test.js

import http from "k6/http";

import { check, group, sleep } from "k6";

import { Rate, Trend } from "k6/metrics";

// Custom metrics

const errorRate = new Rate("errors");

const apiDuration = new Trend("api_duration");

export const options = {

scenarios: {

// Smoke test

smoke: {

executor: "constant-vus",

vus: 3,

duration: "1m",

tags: { test_type: "smoke" },

},

// Load test

load: {

executor: "ramping-vus",

startVUs: 0,

stages: [

{ duration: "3m", target: 50 },

{ duration: "10m", target: 50 },

{ duration: "3m", target: 0 },

],

startTime: "2m",

tags: { test_type: "load" },

},

// Stress test

stress: {

executor: "ramping-vus",

startVUs: 0,

stages: [

{ duration: "2m", target: 50 },

{ duration: "5m", target: 100 },

{ duration: "5m", target: 200 },

{ duration: "5m", target: 300 },

{ duration: "3m", target: 0 },

],

startTime: "20m",

tags: { test_type: "stress" },

},

},

thresholds: {

http_req_duration: ["p(95)<500", "p(99)<1000"],

errors: ["rate<0.05"],

http_req_failed: ["rate<0.01"],

},

};

const BASE_URL = __ENV.BASE_URL || "https://app.arxentra.cloud";

export default function () {

group("API Health", () => {

const res = http.get(`${BASE_URL}/api/health`);

check(res, {

"health status 200": (r) => r.status === 200,

"health response < 100ms": (r) => r.timings.duration < 100,

});

errorRate.add(res.status !== 200);

apiDuration.add(res.timings.duration);

});

group("Service List", () => {

const res = http.get(`${BASE_URL}/api/services`, {

headers: { Authorization: `Bearer ${__ENV.API_TOKEN}` },

});

check(res, {

"services status 200": (r) => r.status === 200,

"services response < 500ms": (r) => r.timings.duration < 500,

});

errorRate.add(res.status !== 200);

});

sleep(Math.random() * 3 + 1);

}

---

Setting Up Locust

Locust is Python-based and better for complex user behavior simulation.

<h1>tests/load/locustfile.py</h1>

from locust import HttpUser, task, between

class WebAppUser(HttpUser):

wait_time = between(1, 5)

host = "https://app.arxentra.cloud"

def on_start(self):

# Login and get session

response = self.client.post("/api/auth/login", json={

"username": "loadtest-user",

"password": "test-password"

})

self.token = response.json().get("token", "")

@task(5)

def view_dashboard(self):

self.client.get("/api/services",

headers={"Authorization": f"Bearer {self.token}"})

@task(2)

def view_service_detail(self):

self.client.get("/api/services/1",

headers={"Authorization": f"Bearer {self.token}"})

@task(1)

def check_health(self):

self.client.get("/api/health")

<h1>Run Locust with web UI</h1>

locust -f tests/load/locustfile.py --users 100 --spawn-rate 10

<h1>Run headless for CI/CD</h1>

locust -f tests/load/locustfile.py --headless \

--users 100 --spawn-rate 10 --run-time 10m \

--csv=results/load-test

---

Baseline Metrics to Track

MetricTargetCritical ThresholdAction if Exceeded

p50 response time< 100ms> 200msInvestigate slow queries
p95 response time< 500ms> 1000msScale or optimize
p99 response time< 1000ms> 3000msEmergency investigation
Error rate< 0.1%> 1%Stop test, investigate
Throughput (RPS)Baseline +/- 10%> 20% dropCheck resource saturation
CPU utilization< 70%> 85%Scale horizontally
Memory utilization< 80%> 90%Check for leaks
DB connection count< pool maxAt pool maxIncrease pool or add PgBouncer

---

Soak Testing for Memory Leaks

Soak tests run at normal load for extended periods to detect memory leaks, connection leaks, and gradual performance degradation.

// k6 soak test configuration

export const options = {

stages: [

{ duration: "5m", target: 50 }, // ramp up

{ duration: "4h", target: 50 }, // sustained load

{ duration: "5m", target: 0 }, // ramp down

],

thresholds: {

http_req_duration: ["p(95)<500"],

http_req_failed: ["rate<0.01"],

},

};

// During the soak test, monitor:

// - RSS memory of application processes (should be stable)

// - PostgreSQL connection count (should not grow)

// - Response time trend (should not increase over time)

// - Disk usage (log files growing unbounded?)

A healthy application shows flat memory usage during a soak test. If memory grows linearly, you have a leak. If response times gradually increase, you have resource exhaustion (connections, file handles, or thread pool).

---

Production-Safe Load Testing

Testing against production requires safeguards to prevent customer impact.

  • Use dedicated test accounts that are excluded from billing and analytics
  • Tag test traffic with a header (`X-Load-Test: true`) so it can be filtered from monitoring
  • Start with 10% of expected traffic and increase gradually
  • Set automatic circuit breakers: if error rate exceeds 5%, stop the test immediately
  • Run during off-peak hours (2-6 AM in your primary user timezone)
  • Monitor customer-facing dashboards during the test, not just test metrics
  • For managed load testing environments, Arxentra provides isolated testing infrastructure. See Cloud Computing for testing environment options.

    ---

    Automated Reporting

    <h1>Generate HTML report from k6 results</h1>
    

    k6 run --out json=results.json tests/load/api-load-test.js

    <h1>Use k6-reporter for HTML output</h1>

    npx k6-reporter results.json --output report.html

    <h1>Post results to Slack</h1>

    curl -X POST "$SLACK_WEBHOOK" \

    -H "Content-Type: application/json" \

    -d "{

    \"text\": \"Load Test Results\",

    \"blocks\": [{

    \"type\": \"section\",

    \"text\": {

    \"type\": \"mrkdwn\",

    \"text\": \"p95: $(jq '.metrics.http_req_duration.values["p(95)"]' results.json)ms

    Errors: $(jq '.metrics.http_req_failed.values.rate' results.json)
    RPS: $(jq '.metrics.http_reqs.values.rate' results.json)\"

    }

    }]

    }"

    ---

    Frequently Asked Questions

    Can I load test my application without a staging environment?

    Yes, but with precautions. Use read-only endpoints for high-volume tests. For write endpoints, use dedicated test accounts with data cleanup afterward. Never run destructive load tests against production without the safeguards listed in the production-safe section above.

    How many virtual users should my load test simulate?

    Start with your actual peak concurrent user count (check analytics). Multiply by 2x for stress testing and 5-10x for breakpoint testing. If you do not know your peak, start with 50 virtual users and increase until you see degradation.

    Should I use k6 or Locust?

    k6 for most teams. It is faster, uses less memory per virtual user, and the JavaScript syntax is familiar to most developers. Use Locust when you need complex Python logic in test scenarios or when your team is Python-first.

    How do I test WebSocket connections under load?

    k6 has native WebSocket support. Create a test that opens WebSocket connections, sends messages at realistic intervals, and validates responses. WebSocket connections consume more server resources than HTTP requests, so test with fewer virtual users.

    What is the difference between throughput and latency?

    Throughput is requests per second (how many). Latency is response time (how fast). High throughput with low latency is the goal. When throughput hits a ceiling, latency starts increasing. This inflection point is your capacity limit. Monitor both during tests. Visit Contact for help with capacity planning.