Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Load Testing with k6

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 Testing with k6 — Core FlowVirtual UsersVUs + durationk6 RunnerJS test scriptTarget APILaravel / RESTMetricsp95, errorsThresholds gate the runhttp_req_failed < 1% | http_req_duration p(95) < 500msFail fast when SLOs break under load
Load testing with k6: virtual users execute scripts against your API and metrics are checked against thresholds.

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

  1. Map the top five user journeys from analytics or product docs.
  2. Assign weights so checkout gets more traffic than the about page.
  3. Add think time with sleep() between requests.
  4. Include POST, PUT, and DELETE—not only GET.
  5. 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.

k6 Staged Load ProfileRamp Up0 → 100 VUsSustain100 VUs × 10mSpike200 VUs × 2mRamp Down200 → 0Watch p95 latency and error rate at each stageDatabase connection pool exhaustion often appears during spikeCompare against MySQL 9.7 slow query log on the server
Staged k6 profiles reveal whether your app survives ramp-up, sustained peak, and sudden spikes.

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.

Criteriak6Apache JMeterLocust
Script languageJavaScript (Go runtime)GUI + JMX/XML; JS optionalPython
CI/CD fitExcellent—single binary, exit codesHeavier; JVM startupGood; needs Python env
Resource efficiencyHigh—Go engineModerate—JVM overheadModerate—Python gevent
Learning curve for devsLow if you know JSSteep for code-first teamsLow for Python shops
Protocol supportHTTP, WebSocket, gRPC, browsers*Very broad plugin ecosystemHTTP primarily
Best fitAPI/load gates in modern CIEnterprise QA with GUI testersPython 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.

Choosing a Load Test ToolAPI + CI team?→ Use k6QA GUI workflows?→ JMeterPython-only shop?→ Locustk6 + Laravel 13 / PHP 8.5 stackGitLab CI → k6 run → threshold fail stops deployPair with rate-limit tests from abuse-prevention guides
Load testing with k6 fits API-first teams running Git-based CI on Laravel and PHP backends.

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.

CI Pipeline with k6 GateUnit TestsPHPUnit / PestIntegrationAPI contractsk6 SmokeThreshold gateDeployZero downtimeCommon gotcha: testing prod by mistakeLock BASE_URL to staging via CI variablesRequire manual approval for stress scenarios
Wire load testing with k6 as a deploy gate after unit and integration tests pass.

Interpreting failures

When thresholds fail, triage in this order:

  1. Confirm staging data volume resembles production—empty tables lie.
  2. Check N+1 queries exposed only under concurrency.
  3. Inspect rate limiting—429 responses inflate http_req_failed.
  4. Review third-party API timeouts during parallel callbacks.
  5. Validate PHP-FPM pm.max_children is 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

Load testing with k6 means writing JavaScript scripts that spawn virtual users, hit your endpoints under rising load, and fail the run when latency or error rates exceed defined thresholds.

Yes. The k6 CLI is open source under AGPL and free to run locally or in CI. Grafana Cloud k6 adds optional hosted execution and dashboards; most Laravel teams run k6 on existing GitLab runners at no extra licensing cost.

Run k6 after feature tests pass but before a major release, infrastructure change, or expected traffic event. Smoke on every main merge; heavier stress and spike profiles on a nightly or pre-release schedule.

On Ubuntu 24.04, add the Grafana apt repository, run apt-get update, then apt-get install k6. macOS users can run brew install k6. Node.js 26 LTS is not required because k6 ships its own JavaScript runtime. Verify with k6 version before writing scripts.

k6 supports smoke tests to confirm scripts work, average-load tests at expected peak, stress tests beyond peak to find breaking points, spike tests for sudden bursts, and soak tests at moderate load over long duration. For booking and checkout flows, spike and soak scenarios often reveal more than a flat ramp.

Create a JavaScript file such as tests/load/smoke.js that imports k6/http and k6 check helpers, sets vus and duration in options, defines thresholds on http_req_failed and http_req_duration, and hits a health endpoint with checks. Run k6 run tests/load/smoke.js. A non-zero exit code means a threshold failed, same as a failing PHPUnit suite.

Laravel 12 and Laravel 13 apps using Sanctum, session cookies, or Passport bearer tokens all work with k6. Use a setup function to POST login credentials and capture a token, pass secrets via environment variables, and attach Authorization headers on subsequent requests. Never hard-code credentials in the repository.

Define thresholds from real SLOs, not wishful targets. Essential built-in metrics include http_req_duration, http_req_failed, http_reqs, vus, and iterations. Tag requests with names like CheckoutPOST so you can threshold individual endpoints. Example gates: http_req_failed rate below 0.01 and p95 latency under 700 ms for a browse scenario.

Map the top five user journeys from analytics, assign weights so checkout gets more traffic than low-value pages, add think time with sleep between requests, and include POST, PUT, and DELETE—not only GET. Test idempotent webhook replay if payments are in scope, because write-heavy checkout paths often fail under concurrency while read endpoints still pass.

k6 wins for developer-centric API teams wanting JavaScript scripts, Git-friendly files, strict threshold gates, and excellent CI fit as a single binary. JMeter suits enterprise QA with GUI testers but carries JVM overhead. Locust fits Python shops but needs a Python environment. k6 supports HTTP, WebSocket, and gRPC at the protocol level.

Add a load stage using the grafana/k6:latest image. Run smoke.js on every merge to main against staging BASE_URL. Schedule heavier stress scripts nightly or pre-release with credentials stored as masked CI variables. Export JSON summaries with k6 run --summary-export=summary.json. Functional tests should pass before k6 smoke runs.

No. k6 measures performance and reliability under load. PHPUnit and Pest verify business logic and functional correctness. Playwright covers browser UX flows. All three belong in a mature pipeline: pair k6 with Laravel feature testing so correctness is proven before capacity testing. Use k6 browser module for headless checks, not as a Playwright replacement.

Staging that mirrors production sizing is the right default. Do not load-test production without explicit approval and strict rate caps. If staging is smaller, document the ratio and extrapolate cautiously. Confirm staging data volume resembles production, because empty tables produce misleading pass results that fail under real concurrency.

Triage in order: confirm staging data volume matches production, check N+1 queries exposed only under concurrency, inspect rate limiting because 429 responses inflate http_req_failed, review third-party API timeouts during parallel callbacks, and validate PHP-FPM pm.max_children is not exhausted. Correlate k6 latency spikes with PHP-FPM queue depth, Redis 8.10 memory, MySQL connections, and disk I/O on the server side.

k6 itself is free and open source. Running k6 from GitLab runners or a dedicated CI agent costs nothing beyond compute you already pay for. Grafana Cloud k6 adds hosted runs and dashboards; budget roughly Rs 3,000–15,000 per month, about USD 22–110, for small teams wanting managed infrastructure instead of self-hosted runners.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: