By Performate
10 Common Load Testing Mistakes (and How to Avoid Them)
Ten recurring load testing failures—from wrong environments to vanity metrics—and how k6-minded teams avoid them.
Your last load test hit staging for five minutes, averaged 180ms, and everyone signed off—then production p99 checkout doubled during a sale. That story repeats because teams optimize for green dashboards instead of honest questions: Did we test the right environment? Did we measure tail latency? Did auth, data, and traffic shape match what customers actually do?
These ten mistakes appear in startups and enterprises alike because performance work touches product, data, infra, and politics at once. In this guide you will learn why vanity metrics and one-shot runs hide risk, how a single k6 script encodes guardrails against the worst failures, and which checklist items turn subjective debates into release gates.
Why load tests fail before the first request
Most failures are process and measurement problems—not missing VUs. Ground fixes in observable behavior: explicit scenarios, tagged metrics, and thresholds aligned with business risk (k6 thresholds, metrics).
- Wrong environment fidelity: tiny staging databases, cold caches, and missing sidecars produce pretty graphs that lie about capacity (capacity planning).
- Vanity metrics: averages and peak VU counts look impressive in slide decks but hide unhappy tails (p95 vs p99).
- Unrealistic traffic: static bearer tokens, shared mutable rows, and zero think time create failures that look like regressions.
- One-shot runs: performance is a distribution; a single Thursday afternoon sample invites false confidence.
- Scope confusion: functional green does not prove contention, pool exhaustion, or GC under parallel load (contract vs performance tests).
Think of load testing like a flight simulator: if the cockpit lacks half the instruments and the runway is half the length, passing the sim says nothing about the real landing.
When the dashboard is green but production is not
Teams often discover gaps only after an incident: they tested /v2 only, ignored OAuth refresh storms, or ran against production without coordination. Cross-check your approach with stress vs load vs spike and an API performance testing checklist so scenario type matches the risk you are actually buying down.
Practical k6 implementation: guardrails against the top ten mistakes
The script below encodes fixes for the mistakes this article names: percentile thresholds instead of averages, tagged routes for diagnosis, env-driven targets (never hard-coded production URLs), realistic think time, and arrival-rate control so load stays honest when the system slows.
Example script (illustrative—not a production-ready test). The snippet below uses fictional URLs, tokens, and SLO numbers. Adapt base URL, auth, payloads, and thresholds to your environment.
What this example demonstrates:
- Tail latency gates: thresholds on
p(95)andp(99)per route—not a single average line that hides checkout pain. - Honest load shape:
constant-arrival-ratekeeps req/s steady even when responses slow (avoids coordinated omission from naive loops). - Tagged diagnostics:
route:searchandroute:checkoutsplit summaries so one slow endpoint does not blur the whole run. - Environment safety:
API_BASEfrom env with a staging default—never embed production URLs in committed scripts.
import http from 'k6/http';
import { check, sleep } from 'k6';
import { SharedArray } from 'k6/data';
const BASE = __ENV.API_BASE || 'https://staging.example.com';
const TOKEN = __ENV.TOKEN || 'staging-token-replace-me';
// Mistake #6 fix: unique-ish SKUs per VU iteration, not one shared row
const skus = new SharedArray('skus', () =>
JSON.parse(open('./data/skus.json')).map((s) => s.id)
);
export const options = {
scenarios: {
// Mistake #9 fix: target req/s, not vanity max VUs
browse: {
executor: 'constant-arrival-rate',
rate: Number(__ENV.SEARCH_RPS || 20),
timeUnit: '1s',
duration: '8m',
preAllocatedVUs: 15,
maxVUs: 60,
tags: { route: 'search' },
exec: 'searchFlow',
},
checkout: {
executor: 'constant-arrival-rate',
rate: Number(__ENV.CHECKOUT_RPS || 5),
timeUnit: '1s',
duration: '8m',
preAllocatedVUs: 10,
maxVUs: 40,
tags: { route: 'checkout' },
exec: 'checkoutFlow',
},
},
thresholds: {
// Mistake #3 fix: percentiles, not averages
'http_req_duration{route:search}': ['p(95)<400', 'p(99)<700'],
'http_req_duration{route:checkout}': ['p(95)<800', 'p(99)<1200'],
http_req_failed: ['rate<0.01'],
// Mistake #4 fix: dropped iterations surface silent load drops
dropped_iterations: ['count==0'],
},
};
export function searchFlow() {
const q = `sku-${__VU}-${__ITER}`;
const res = http.get(`${BASE}/search?q=${q}`, {
headers: { Authorization: `Bearer ${TOKEN}` },
tags: { route: 'search' },
});
check(res, { 'search 2xx': (r) => r.status >= 200 && r.status < 300 });
sleep(Math.random() * 1.5 + 0.5); // Mistake #8 fix: think time
}
export function checkoutFlow() {
const sku = skus[(__VU + __ITER) % skus.length];
const body = JSON.stringify({ sku, qty: 1 });
const res = http.post(`${BASE}/checkout`, body, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${TOKEN}`,
},
tags: { route: 'checkout' },
});
check(res, { 'checkout 2xx': (r) => r.status >= 200 && r.status < 300 });
sleep(1);
}
Patterns that work
- Run the same script twice on different nights or builds before declaring victory—compare baseline regression exports.
- Document environment gaps next to every chart (DB size ratio, cache warm state, mesh on/off).
- Refresh tokens realistically when OAuth is in path (OAuth concurrency).
- Review AI-generated scripts like production code: endpoints, secrets, and assertions before scaling (AI safety checklist).
Anti-patterns to avoid
- Hitting production without a written window, abort contact, and blast-radius review.
- Cloning scripts per microservice instead of tagging routes in one honest mix.
- Treating max VUs as a success metric without a hypothesis tied to arrival rate or concurrency.
- Ignoring
dropped_iterationswhen the system slows and your loop silently skips work.
Pro tip (example command): the line below is an example of how to surface tail latency and threshold failures in one summary—not a mandatory flag for every run.
k6 run guardrails.js --summary-trend-stats="p(95),p(99)" --thresholds-on-summary
What this command demonstrates: k6 prints percentile trends and highlights which route tags failed which SLO—so reviewers debate thresholds, not screenshots of averages.
Decision framework: which mistake to fix first
| Situation | Recommended action |
|---|---|
| Leadership asks "how many users can we handle?" | Switch from max-VU bragging to arrival-rate or open-model scenarios with documented assumptions |
| Staging is much smaller than prod | Label results "directional only"; invest in prod-like data volume or dedicated perf env |
| p99 spikes but mean looks fine | Add percentile thresholds per route; stop reporting mean latency in release notes |
| Failures cluster on one endpoint | Tag routes in k6 and APM; fix hot path before scaling total RPS |
| Team runs one 5-minute test before release | Require two runs + baseline diff; add CI smoke at low rate (CI/CD load testing) |
Use arrival-rate executors if you need honest req/s when response times stretch—typical for API SLO work.
Use repeated runs with frozen scripts if you are building a regression baseline across releases or infrastructure changes.
Use strict env defaults and checklists if your organization has ever accidentally pointed a script at production—or debated whether it "really happened."
Observability, documentation, and next steps
Reliable load tests survive handoffs to QA, platform, and finance. Before you scale traffic:
- Document environment fidelity gaps (DB size, cache state, region, mesh) beside every exported summary.
- Set percentile thresholds per critical route—not one global
http_req_durationline. - Track
dropped_iterationsand failed checks alongside latency; silent load drops invalidate throughput claims. - Correlate k6 route tags with APM and logs (correlation IDs) before blaming "the network."
- Archive script hash, env file (redacted), and git SHA per run so regressions compare apples to apples.
How Performate reduces repetitive setup mistakes
Copy-pasting executor blocks and re-importing Postman folders every sprint is how mistakes #5 and #6 creep back in. Below is a concrete workflow example for the search + checkout flows this article discusses.
Example: from collection import to repeatable guardrail run
- Import one Postman collection with
searchandcheckoutrequests—or OpenAPI with both paths. Problem solved: one source of truth instead of forked scripts that drift within two sprints. - Create two scenarios in the visual editor—
browseat 20 req/s andcheckoutat 5 req/s—matching production mix, not vanity VU counts. Problem solved: honest traffic shape without hand-editing executor YAML each tuning pass. - Apply route tags (
route:search,route:checkout) in the scenario panel so reports split latency the same way the k6 example does. Problem solved: on-call and QA filter one export instead of three spreadsheets. - Set percentile thresholds in the UI aligned to your SLO doc—p95/p99 per route, error rate under 1%. Problem solved: release debates reference thresholds, not average latency screenshots.
- Run twice before sign-off and use the comparison view across nights or builds. Problem solved: one-shot false confidence becomes visible drift.
- Export the generated k6 script for CI smoke gates so local tuning and pipeline runs stay aligned. Problem solved: the same guardrails run in the IDE and in GitHub Actions.
That workflow maps directly to the cta in this post: fewer repetitive setup errors, more reliable tests every release.
Closing takeaway
Load testing mistakes are predictable—wrong environment, wrong metric, wrong traffic shape, wrong number of runs. Encode guardrails in one script: percentile thresholds, route tags, env-driven targets, and arrival rates that stay honest when the system slows.
Run your next test twice, tag every critical route, and report p99 alongside the mean—then note which of the ten mistakes your team still practices by habit.
Ready to optimize your API performance?
Use Performate workflows to avoid repetitive setup errors and run more reliable tests.