Skip to content
Load Testing with k6: Scripts, Virtual Users, Thresholds, and Browser Testing

Load Testing with k6: Scripts, Virtual Users, Thresholds, and Browser Testing

DodaTech Updated Jun 20, 2026 8 min read

k6 is a modern, developer-friendly load testing tool that uses JavaScript to define test scenarios — making performance testing accessible to developers who already know JavaScript.

What You’ll Learn

  • Writing k6 test scripts with JavaScript
  • Configuring virtual users, ramp-up stages, and test duration
  • Setting performance thresholds that act as quality gates
  • Browser-level testing with k6 browser
  • Integrating k6 into CI/CD pipelines

Why k6 Matters

Traditional load testing tools like JMeter use XML-based test plans that are hard to version control and review. k6 uses JavaScript — a language developers already know — with a clean API for defining load patterns, checks, and thresholds. It’s designed for CI/CD integration from the ground up, supports cloud execution, and includes browser-level testing for measuring frontend performance.

Doda Browser uses k6 to load test its sync service — simulating thousands of concurrent users syncing bookmarks, passwords, and settings across devices.

Learning Path

    flowchart LR
  A[Performance Testing] --> B[k6 Load Testing<br/>You are here]
  B --> C[CI/CD Integration]
  C --> D[Production Monitoring]
  D --> E[Capacity Planning]
  style B fill:#f90,color:#fff
  

Setup

# Install k6 (macOS)
brew install k6

# Linux (Debian/Ubuntu)
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb 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

# Windows
winget install k6

Your First Test

// first-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  vus: 10,           // 10 virtual users
  duration: '30s',   // Run for 30 seconds
};

export default function () {
  const res = http.get('https://test.k6.io');
  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
  });
  sleep(1);
}

Run it:

k6 run first-test.js

Expected output:

          /\      |‾‾| /‾‾/   /‾‾/
     /\  /  \     |  |/  /   /  /
    /  \/    \    |     (   /   ‾‾\
   /          \   |  |\  \ |  (‾)  |
  / __________ \  |__| \__\ \_____/ .io

  execution: local
     script: first-test.js
     output: -

  scenarios: (100.00%) 1 scenario, 10 max vus, 1m0s max duration (incl. graceful stop):
           * default: 10 looping users for 30s (startStages: 0s, gracefulStop: 30s)


     ✓ status is 200
     ✓ response time < 500ms

     checks.........................: 100.00% ✓ 280       ✗ 0
     data_received..................: 2.1 MB  69 kB/s
     http_req_blocked...............: avg=1.2ms   p(95)=4.1ms
     http_req_connecting............: avg=0.8ms   p(95)=3.2ms
     http_req_duration..............: avg=98.3ms  p(95)=187.2ms
     http_req_receiving.............: avg=0.3ms   p(95)=0.6ms
     http_req_sending...............: avg=0.1ms   p(95)=0.2ms
     http_req_tls_handshaking......: avg=0.8ms   p(95)=3.5ms
     http_req_waiting...............: avg=97.1ms  p(95)=184.3ms
     http_req_failed................: 0.00%  ✓ 0         ✗ 280
     vus............................: 10     min=10      max=10
     vus_max........................: 10     min=10      max=10

Virtual Users and Stages

Control load patterns with stages:

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '2m', target: 50 },   // Ramp up to 50 users
    { duration: '5m', target: 50 },   // Stay at 50 users
    { duration: '2m', target: 100 },  // Ramp up to 100
    { duration: '5m', target: 100 },  // Stay at 100
    { duration: '2m', target: 0 },    // Ramp down
  ],
};

Use Cases

PatternPurpose
Ramp-up onlyFind the breaking point
Steady stateVerify sustained load handling
SpikeSudden traffic surge
Ramp-downVerify graceful shutdown

Checks and Thresholds

Checks

Checks verify conditions during the test but don’t stop it:

import http from 'k6/http';
import { check } from 'k6';

export default function () {
  const res = http.get('https://api.example.com/users');

  check(res, {
    'status is 200': (r) => r.status === 200,
    'has users array': (r) => Array.isArray(r.json()),
    'response time < 300ms': (r) => r.timings.duration < 300,
  });
}

Thresholds

Thresholds define pass/fail criteria for the entire test:

export const options = {
  thresholds: {
    http_req_duration: [
      'p(95)<500',     // 95% of requests under 500ms
      'p(99)<1500',    // 99% under 1.5s
    ],
    http_req_failed: ['rate<0.01'],  // Less than 1% failures
    checks: ['rate>0.95'],           // At least 95% checks pass
  },
};

Thresholds can fail the build in CI:

k6 run script.js
# If thresholds are exceeded, k6 exits with non-zero code
# This can fail a CI pipeline

Custom Metrics

Define custom metrics for specific business requirements:

import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend, Counter } from 'k6/metrics';

const errorRate = new Rate('errors');
const loginDuration = new Trend('login_duration');
const ordersCreated = new Counter('orders_created');

export default function () {
  // Login
  const loginRes = http.post('https://api.example.com/login', {
    username: 'test_user',
    password: 'test_pass',
  });

  loginDuration.add(loginRes.timings.duration);

  if (loginRes.status === 200) {
    const token = loginRes.json().token;

    // Create order
    const orderRes = http.post(
      'https://api.example.com/orders',
      JSON.stringify({ product: 'book', quantity: 1 }),
      { headers: { Authorization: `Bearer ${token}` } }
    );

    if (orderRes.status === 201) {
      ordersCreated.add(1);
    }

    errorRate.add(orderRes.status !== 201);
  } else {
    errorRate.add(1);
  }

  sleep(1);
}

Browser Testing

k6 can measure browser-level performance using the browser module:

import { browser } from 'k6/experimental/browser';
import { check } from 'k6';

export const options = {
  scenarios: {
    ui: {
      executor: 'shared-iterations',
      options: {
        browser: {
          type: 'chromium',
        },
      },
    },
  },
  thresholds: {
    browser_web_vital_inp: ['p(75)<200'],  // Interaction to Next Paint
    browser_web_vital_lcp: ['p(75)<2500'], // Largest Contentful Paint
  },
};

export default async function () {
  const page = browser.newPage();

  try {
    await page.goto('https://example.com');

    // Measure real user metrics
    check(page, {
      'LCP is acceptable': () =>
        page.metrics().largestContentfulPaint < 2500,
    });

    // Interact with the page
    await page.fill('#search', 'performance testing');
    await page.click('button[type="submit"]');
    await page.waitForSelector('.results');

    check(page, {
      'results are visible': () =>
        page.locator('.results').isVisible(),
    });
  } finally {
    page.close();
  }
}

CI/CD Integration

# .github/workflows/load-test.yml
name: Load Test
on:
  schedule:
    - cron: '0 6 * * 1'  # Every Monday at 6 AM
  workflow_dispatch:       # Manual trigger

jobs:
  load-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run k6 load test
        uses: grafana/k6-action@v0.2.0
        with:
          filename: tests/load/load-test.js
          flags: --out json=results.json

      - name: Upload results
        uses: actions/upload-artifact@v4
        with:
          name: k6-results
          path: results.json

      - name: Check thresholds
        run: |
          if jq -e '.metrics.http_req_duration.values["p(95)"] > 500' results.json; then
            echo "Performance threshold exceeded!"
            exit 1
          fi

Common k6 Mistakes

1. Not Warming Up

Starting at full load immediately can skew results — caches are empty, connections aren’t pooled, JIT hasn’t warmed up.

Fix: Include a ramp-up stage before measurement stages.

2. Testing Only One Endpoint

Testing only the health-check endpoint doesn’t represent real user behavior.

Fix: Simulate realistic user journeys — login, browse, search, interact.

3. No sleep() Between Requests

Without sleep(), virtual users send requests as fast as possible, which may overwhelm the system unrealistically.

Fix: Add realistic think time with sleep() between user actions.

4. Not Validating Responses

Checking only status codes misses response errors, wrong data, or slow responses.

Fix: Use check() to validate response bodies and timing.

5. Ignoring Thresholds in CI

Running load tests without thresholds means they never fail the build.

Fix: Set meaningful thresholds that reflect your performance requirements.

6. Testing Against Staging That Differs From Production

If staging has half the CPU or a different database, results won’t predict production behavior.

Fix: Match staging to production as closely as possible.

7. Not Measuring Both Sides

Server-side response time is only half the story. Browser-level testing measures what the user actually experiences.

Fix: Combine API-level load testing with browser-level user experience measurement.

Practice Questions

1. What is a virtual user in k6?

A simulated user that executes the default function in a loop, making HTTP requests and performing checks.

2. What is the difference between checks and thresholds?

Checks verify conditions during the test. Thresholds define pass/fail criteria for the entire test run. Thresholds can fail the build.

3. Why should you include ramp-up stages in load tests?

To warm up the system (caches, connections, JIT) and find the load level where performance degrades.

4. What are Web Vitals in browser testing?

Metrics that measure real user experience: LCP (loading), INP (interactivity), CLS (visual stability).

5. How do you integrate k6 into a CI/CD pipeline?

Use the k6-action GitHub Action or run k6 run directly, with thresholds that exit non-zero on failure.

Challenge: Write a k6 script that simulates a realistic e-commerce user journey: search for a product, view product details, add to cart, and checkout. Include thresholds for response times and error rates.

FAQ

How is k6 different from JMeter?
k6 uses JavaScript for test scripting and is designed for CI/CD from the start. JMeter uses XML and a GUI. k6 is more developer-friendly; JMeter has more enterprise features.
Can k6 test APIs behind authentication?
Yes. You can obtain tokens in the setup function or use environment variables for API keys.
How many virtual users can k6 simulate?
k6 can simulate hundreds of thousands of virtual users on a single machine, depending on the test complexity.
Does k6 support distributed testing?
Yes, through k6 Cloud or the k6 Operator for Kubernetes. The open-source version runs on a single machine.
What is the k6 browser module?
An experimental module that runs real Chromium browsers for measuring frontend performance, including Web Vitals.

What’s Next

TutorialWhat You’ll Learn
Performance Testing StrategiesComprehensive performance testing guide
k6 Cloud and Distributed TestingRunning large-scale tests with k6 Cloud
JMeter Performance TestingEnterprise performance testing with JMeter

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Updated 2026-06-20.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro