
September 10, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
Production traffic rarely arrives on a schedule you control. A booking spike, a payment callback storm, or a marketing push can turn a stable REST API into a slow, error-prone mess within minutes. Load testing with k6 gives you a scriptable way to simulate that traffic early, on staging, with metrics you can trust. This guide walks through install, script design, thresholds, and CI wiring—the same workflow I use before shipping Laravel apps and high-traffic eCommerce endpoints.
What is load testing with k6 and when should you run it?
k6 is an open-source load testing tool from Grafana Labs. You write tests in JavaScript, run them from the CLI or CI, and export results to Grafana Cloud, InfluxDB, or plain JSON. Unlike browser-only tools, k6 focuses on protocol-level HTTP, WebSocket, and gRPC traffic. That makes it ideal for API and backend teams.
Run k6 after feature tests pass but before a major release, infrastructure change, or expected traffic event. On a production Laravel application, I typically load-test login, checkout, search, and webhook endpoints—the paths that mix auth, database writes, and third-party calls. Pair this with Laravel feature testing so functional correctness is already proven.
Do not load-test production without explicit approval and strict rate caps. Staging that mirrors production sizing is the right default. If staging is smaller, document the ratio and extrapolate cautiously.
Load test types k6 supports
- Smoke test — a handful of VUs to confirm the script and environment work.
- Average-load test — sustained traffic at expected peak levels.
- Stress test — ramp beyond expected peak to find the breaking point.
- Spike test — sudden burst to mimic viral traffic or flash sales.
- Soak test — long duration at moderate load to catch memory leaks.
Most teams under-test spikes and over-test gentle ramps. For booking platforms and checkout flows, spike and soak scenarios often reveal more than a flat 100-VU run.
How do you install k6 and run your first load test?
Install k6 on Ubuntu, macOS, or Windows. On Ubuntu 24.04, the Grafana apt repository is the cleanest path. Node.js 26 LTS is not required—k6 ships its own JavaScript runtime.
Install on Ubuntu
sudo gpg -k
sudo gpg --no-default-keyring \
--keyring /usr/share/keyrings/k6-archive-keyring.gpg \
--keyserver hkp://keyserver.ubuntu.com:80 \
--recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
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 version macOS users can run brew install k6. Verify with k6 version before writing scripts.
Your first script
Create smoke.js in your project’s tests/load/ directory:
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 5,
duration: '30s',
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<800'],
},
};
export default function () {
const res = http.get('https://staging.example.com/api/health');
check(res, {
'status is 200': (r) => r.status === 200,
'body mentions ok': (r) => r.body.includes('ok'),
});
sleep(1);
} Run it:
k6 run tests/load/smoke.js k6 prints a summary table with request rate, latency percentiles, and threshold pass/fail status. A non-zero exit code means at least one threshold failed—wire that into CI the same way you treat a failing PHPUnit suite.
Validate JSON responses with the same discipline you apply in unit tests. A JSON formatter helps when you paste failing payloads into tickets during triage.
How do you write k6 scripts for Laravel APIs and authenticated flows?
Laravel 13 and Laravel 12 apps expose Sanctum tokens, session cookies, or Passport bearer tokens. k6 handles all three. The pattern I use on client projects: one setup function for auth, a default function for the hot path, and shared helpers for headers.
Testing a public JSON endpoint
import http from 'k6/http';
import { check, sleep } from 'k6';
const BASE = __ENV.BASE_URL || 'https://staging.example.com';
export const options = {
stages: [
{ duration: '2m', target: 50 },
{ duration: '5m', target: 50 },
{ duration: '2m', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<600'],
http_req_failed: ['rate<0.005'],
},
};
export default function () {
const res = http.get(`${BASE}/api/products?limit=20`, {
headers: { Accept: 'application/json' },
});
check(res, {
'status 200': (r) => r.status === 200,
'has data array': (r) => JSON.parse(r.body).data !== undefined,
});
sleep(Math.random() * 2 + 1);
} Bearer token auth with setup()
import http from 'k6/http';
import { check, sleep } from 'k6';
const BASE = __ENV.BASE_URL;
const TOKEN = __ENV.API_TOKEN;
export function setup() {
const login = http.post(`${BASE}/api/login`, JSON.stringify({
email: __ENV.TEST_EMAIL,
password: __ENV.TEST_PASSWORD,
}), { headers: { 'Content-Type': 'application/json' } });
check(login, { 'login ok': (r) => r.status === 200 });
return { token: login.json('token') };
}
export default function (data) {
const res = http.get(`${BASE}/api/bookings`, {
headers: {
Authorization: `Bearer ${data.token}`,
Accept: 'application/json',
},
});
check(res, { 'bookings 200': (r) => r.status === 200 });
sleep(1);
} Pass secrets via environment variables—never hard-code credentials in the repo. Use a dedicated test account with realistic but disposable data. For deeper PHP-specific patterns, see the companion piece on load testing with k6 for PHP apps.
Modelling realistic user behaviour
- Map the top five user journeys from analytics or product docs.
- Assign weights so checkout gets more traffic than the about page.
- Add think time with
sleep()between requests. - Include POST, PUT, and DELETE—not only GET.
- Test idempotent webhook replay if payments are in scope.
Payment callbacks and SMS gateways are common failure points. I've seen load tests pass on read-heavy endpoints while POST-heavy checkout paths fail under concurrent writes. Test the write path explicitly.
What k6 metrics and thresholds should you define?
Thresholds turn k6 from a benchmarking toy into a release gate. Define them from real SLOs—not from wishful thinking. If product expects sub-500 ms checkout, encode that as p(95)<500 on the checkout group.
Essential built-in metrics
- http_req_duration — total request time including DNS, TLS, and TTFB.
- http_req_failed — rate of failed requests (4xx/5xx or network errors).
- http_reqs — throughput; useful for capacity planning.
- vus — active virtual users at a given moment.
- iterations — completed script loops; tracks scenario completion.
Example threshold block
export const options = {
scenarios: {
browse: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '3m', target: 80 },
{ duration: '10m', target: 80 },
{ duration: '2m', target: 0 },
],
gracefulRampDown: '30s',
},
},
thresholds: {
http_req_failed: ['rate<0.01'],
'http_req_duration{scenario:browse}': ['p(95)<700', 'p(99)<1200'],
checks: ['rate>0.99'],
},
}; Tag requests with tags: { name: 'CheckoutPOST' } so you can threshold individual endpoints. That matters when one slow query poisons an otherwise healthy API surface.
While k6 runs, watch server-side signals in parallel: PHP-FPM queue depth, Redis 8.10 memory, MySQL connections, and disk I/O. Application metrics explain why p95 jumped—not just that it did. Our testing and optimization service often starts with this server-side correlation after a failed k6 run.
How does k6 compare to JMeter, Locust, and artillery?
Tool choice depends on team skills, CI fit, and protocol needs. k6 wins for developer-centric API teams that want JavaScript, Git-friendly scripts, and strict threshold gates.
| Criteria | k6 | Apache JMeter | Locust |
|---|---|---|---|
| Script language | JavaScript (Go runtime) | GUI + JMX/XML; JS optional | Python |
| CI/CD fit | Excellent—single binary, exit codes | Heavier; JVM startup | Good; needs Python env |
| Resource efficiency | High—Go engine | Moderate—JVM overhead | Moderate—Python gevent |
| Learning curve for devs | Low if you know JS | Steep for code-first teams | Low for Python shops |
| Protocol support | HTTP, WebSocket, gRPC, browsers* | Very broad plugin ecosystem | HTTP primarily |
| Best fit | API/load gates in modern CI | Enterprise QA with GUI testers | Python backends, custom logic |
*k6 browser module adds headless Chromium checks but is not a replacement for Playwright E2E suites. Use Playwright in CI for UI flows and k6 for API throughput.
How do you run load testing with k6 in CI/CD pipelines?
Keep smoke k6 runs on every merge to main. Schedule heavier stress tests nightly or pre-release. Never point unconstrained stress tests at shared staging without coordinating with ops— you'll starve other teams and skew results.
GitLab CI example
stages:
- test
- load
k6_smoke:
stage: load
image: grafana/k6:latest
variables:
BASE_URL: "https://staging.example.com"
script:
- k6 run tests/load/smoke.js
rules:
- if: $CI_COMMIT_BRANCH == "main"
k6_stress:
stage: load
image: grafana/k6:latest
variables:
BASE_URL: "https://staging.example.com"
TEST_EMAIL: $K6_TEST_EMAIL
TEST_PASSWORD: $K6_TEST_PASSWORD
script:
- k6 run tests/load/booking-stress.js
rules:
- if: $CI_PIPELINE_SOURCE == "schedule" Store credentials in CI masked variables. Export JSON summary for artefacts:
k6 run --summary-export=summary.json tests/load/smoke.js This mirrors the Deployer 7 + GitLab CI pipelines I maintain on shared EC2 infrastructure. Functional tests run first; k6 smoke follows; full stress waits for scheduled windows. See integration testing in CI pipelines for the broader test ordering strategy.
Load balancers and multi-node staging
If staging sits behind HAProxy or another load balancer, run k6 against the VIP—not individual nodes—unless you deliberately test node affinity. Warm up opcache and Redis caches with a short ramp before recording metrics.
Interpreting failures
When thresholds fail, triage in this order:
- Confirm staging data volume resembles production—empty tables lie.
- Check N+1 queries exposed only under concurrency.
- Inspect rate limiting—429 responses inflate
http_req_failed. - Review third-party API timeouts during parallel callbacks.
- Validate PHP-FPM
pm.max_childrenis not exhausted.
Rate limits deserve their own test plan. Read API rate limiting and abuse prevention before tuning thresholds around 429 responses.
Cost and infrastructure notes
k6 itself is free and open source. Grafana Cloud k6 adds hosted runs and dashboards—budget roughly Rs 3,000–15,000/month (~USD 22–110) for small teams if you want managed infra. Running k6 from GitLab runners or a dedicated CI agent costs nothing beyond compute you already pay for.
For Linux server tuning, load tests justify raising connection limits and validating Redis 8.10 eviction policy before a sale event. Document baseline p95 after each infrastructure change so regressions are obvious.
Key Takeaways
- Install k6 as a single binary, write JavaScript scripts, and fail the run with thresholds tied to real SLOs.
- Test authenticated write paths—not just GET health checks—and model spikes plus soak duration.
- Pass secrets via environment variables; never commit credentials or aim stress tests at production without approval.
- Run k6 smoke on every main-branch merge; schedule heavier profiles nightly or pre-release.
- Correlate k6 latency spikes with PHP-FPM, MySQL, and Redis metrics on the server side.
- Pair k6 with functional, integration, and E2E tests—load testing proves capacity, not correctness.
People Also Ask
Is k6 free for load testing?
Yes. The k6 CLI is open source under AGPL and free to run locally or in CI. Grafana Cloud k6 offers optional hosted execution and dashboards. Most Laravel teams I work with run k6 on existing GitLab or GitHub runners at no extra licensing cost.
Can k6 test Laravel Sanctum and session-based auth?
Yes. Use setup() to obtain a bearer token or capture session cookies from a login POST. Pass the token or cookie jar on subsequent requests. For cookie sessions, enable jar: true in k6 HTTP options so cookies persist across VU iterations.
How many virtual users do I need for a meaningful load test?
Start with enough VUs to match expected concurrent users, not total daily visits. If peak concurrency is 200 users, ramp to 200 VUs and hold. Then run a spike to 400 VUs to find headroom. Adjust based on think time—k6 VUs are concurrent script executors, not exact human counts.
Does k6 replace PHPUnit, Pest, or Playwright?
No. k6 measures performance and reliability under load. PHPUnit and Pest verify business logic. Playwright covers browser UX. All three belong in a mature pipeline—see the test automation strategy pyramid for how they layer together.
Ship with confidence after load testing with k6
Load testing with k6 is the fastest way I've found to turn "it worked on my machine" into measurable capacity data before launch. Start with a 30-second smoke script, add thresholds that match your SLOs, and wire the run into CI so regressions block deploys automatically. When you need help designing scenarios for a Laravel booking flow, WooCommerce checkout, or high-traffic legal-tech portal, review our portfolio and reach out via contact us—or explore speed optimization if k6 already exposed the bottlenecks.
Official reference: the Grafana k6 documentation covers executors, scenarios, and cloud options in full. For HTTP semantics and status codes during check design, keep the MDN HTTP status reference handy. Laravel teams should also read the Laravel testing guide to keep functional and load suites aligned.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

