Table of contents
โšก Quick answer

k6 is an open-source, developer-centric load testing tool: you write test scripts in JavaScript, run them from the command line, and k6 simulates many virtual users hitting your API at once while measuring response times, error rates, and throughput. To get started: install the k6 binary, write a script that sends a request inside a default function, define ramping stages for how many virtual users to simulate over time, add thresholds so the run passes or fails automatically, then wire that same command into your CI/CD pipeline so performance regressions are caught before release.

Key takeaways
  • k6 scripts are plain JavaScript, so they live in your codebase and get reviewed like any other test.
  • Virtual users (VUs) simulate concurrent clients โ€” start small and ramp up gradually to find real breaking points.
  • Thresholds turn a load test into a pass/fail gate instead of a report someone has to read manually.
  • p(95) response time matters more than the average โ€” it shows what your slowest real users actually experience.
  • Always target staging or a dedicated performance environment, never production, for heavy load runs.

Most teams don't find out their API can't handle real traffic until it's already happening in production. Load testing closes that gap โ€” and k6 has become the tool of choice for doing it, because it treats performance tests like code: version-controlled, scriptable, and runnable in CI/CD alongside your functional test suite.

This guide walks through installing k6, writing and running your first script, understanding the metrics it reports, and building toward a load test suite that runs automatically before every release.

What Is k6 and Why Use It?

k6 is an open-source load testing tool built by Grafana Labs. Instead of a drag-and-drop GUI, you write test scripts in JavaScript that describe what a simulated user does โ€” which endpoints they hit, what data they send, how long they wait between actions. k6 then runs many of these scripts concurrently as "virtual users" and reports on how your system performed under that load.

Compared to older tools, a few things stand out:

  • Code-first workflow. Scripts are JavaScript files that live in git, get reviewed in pull requests, and run the same way locally and in CI.
  • Low resource overhead. k6's engine is written in Go, so a single machine can simulate thousands of virtual users without the memory overhead of thread-per-user tools.
  • Built-in thresholds. You can define pass/fail performance criteria directly in the script, turning a load test into an automated gate rather than a report someone reviews after the fact.

If your team is deciding between purely functional API checks and performance validation, our load testing with k6 service covers exactly this kind of setup for production APIs.

Installing k6

k6 installs as a standalone binary โ€” no runtime dependency to manage beyond the tool itself.

Shell ยท Install
# macOS (Homebrew)
brew install k6

# Windows (Chocolatey)
choco install k6

# Debian/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

# Or run it with Docker โ€” no install needed
docker run --rm -i grafana/k6 run - 

Verify the install with k6 version. If you'd rather skip local setup entirely, the Docker command above works identically in CI.

Your First Load Test

A k6 script needs just two things: an import for the HTTP module, and a default function that describes what each virtual user does per iteration.

JavaScript ยท k6
// load-test.js
import http from 'k6/http';
import { sleep, check } from 'k6';

export const options = {
  vus: 10,
  duration: '30s',
};

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

  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
  });

  sleep(1);
}

Run it with:

Shell
k6 run load-test.js

This simulates 10 virtual users hitting the endpoint continuously for 30 seconds, each pausing 1 second between requests, and prints a summary of response times, request rate, and check pass/fail counts when it finishes.

Understanding Virtual Users and Stages

A fixed VU count is fine for a quick smoke check, but real traffic ramps up and down. The stages option lets you model that:

JavaScript ยท k6
export const options = {
  stages: [
    { duration: '1m', target: 20 },   // ramp up to 20 VUs
    { duration: '3m', target: 20 },   // hold at 20 VUs
    { duration: '1m', target: 50 },   // spike to 50 VUs
    { duration: '2m', target: 50 },   // hold at peak
    { duration: '1m', target: 0 },    // ramp down
  ],
};

This pattern โ€” ramp up, hold, spike, hold, ramp down โ€” mirrors how real traffic behaves and reveals problems a flat VU count won't: connection pool exhaustion under sustained load, or a system that copes fine at steady state but falls over during a sudden spike.

๐Ÿ’ก Practical tip Start with a low VU count and short duration to validate the script logic itself works correctly before scaling up. A script bug at 200 VUs is much harder to diagnose than the same bug at 5 VUs.

Key k6 Metrics Explained

MetricWhat it meansWhy it matters
http_req_durationTotal time for a request to completeCore latency signal โ€” watch avg and percentiles, not just avg
http_req_duration{p(95)}95th percentile response timeShows what your slowest 5% of real users experience
http_req_failedRatio of failed requestsCatches errors that a slow-but-successful response would hide
vusActive virtual users at a point in timeCorrelate against latency/error spikes to find the breaking point
iterationsTotal completed script runsCombined with duration, gives you real throughput

Averages hide problems. A 200ms average response time sounds healthy, but if the p(95) is 4 seconds, one in twenty real users is having a bad experience โ€” always check percentiles, not just the mean.

Adding Checks and Thresholds

Checks validate individual responses (like an assertion); thresholds validate the entire test run and control the exit code, which is what makes a load test usable as a CI gate.

JavaScript ยท k6
export const options = {
  stages: [
    { duration: '1m', target: 20 },
    { duration: '3m', target: 20 },
    { duration: '1m', target: 0 },
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'],   // 95% of requests under 500ms
    http_req_failed: ['rate<0.01'],     // error rate under 1%
  },
};

With thresholds set, k6 run exits with a non-zero status if either condition fails โ€” the same signal a CI pipeline checks to decide whether to block a deploy.

Real-World Test Scenarios

Testing multiple endpoints in one script

Real user sessions touch several endpoints, not one. Group related requests inside the default function, and use group() to label them for clearer reporting:

JavaScript ยท k6
import { group, sleep } from 'k6';
import http from 'k6/http';

export default function () {
  group('browse products', () => {
    http.get('https://api.example.com/products');
    sleep(1);
  });

  group('view product detail', () => {
    http.get('https://api.example.com/products/101');
    sleep(2);
  });
}

Data-driven load with realistic payloads

Load a JSON or CSV file of test data with SharedArray so every virtual user isn't sending the exact same request โ€” closer to how real traffic varies.

Authenticated flows

For APIs behind auth, log in once per virtual user at the start of the script, capture the token, and attach it to subsequent requests via headers โ€” mirroring how a real session behaves rather than testing an unauthenticated endpoint.

Running k6 in CI/CD

Once thresholds are in place, a load test becomes a deployment gate, not a manual chore:

YAML ยท GitHub Actions
name: Load Test
on:
  workflow_dispatch:
  schedule:
    - cron: '0 3 * * *'
jobs:
  k6-load-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run k6 load test
        uses: grafana/k6-action@v0.3.1
        with:
          filename: load-test.js
        env:
          BASE_URL: ${{ secrets.STAGING_API_URL }}

Heavy load tests usually run on a schedule (nightly) or on demand rather than on every pull request, since they take longer and consume more staging capacity than a functional suite. Teams that want full setup โ€” realistic traffic modeling, staging capacity planning, and dashboards โ€” are exactly who our k6 load testing service is built for.

Best Practices

  • Always target staging or a dedicated performance environment โ€” never run heavy load against production.
  • Start with low VUs and short duration to validate script correctness before scaling up.
  • Check percentiles (p95, p99), not just averages โ€” averages hide the worst experiences.
  • Model realistic think time with sleep() between actions instead of hammering endpoints back-to-back.
  • Set thresholds so the test can pass or fail automatically, not just produce a report.
  • Version-control scripts alongside the API code they test, and review changes like any other test.
  • Re-baseline thresholds after intentional infrastructure or scaling changes so old numbers don't cause false failures.

Frequently Asked Questions

Yes. The k6 CLI and scripting engine are open source and free to run locally or in your own CI/CD pipeline. Grafana Cloud k6 adds optional paid cloud execution and long-term result storage, but it isn't required to write or run load tests.

A virtual user (VU) is a simulated client running your script in a loop; requests per second (RPS) is the resulting throughput, which depends on both the number of VUs and how long each iteration takes. Doubling VUs doesn't always double RPS if the target system or script has bottlenecks.

Start small โ€” 5 to 10 virtual users โ€” to confirm the script itself works correctly, then ramp up gradually while watching error rates and response times. Jumping straight to a large VU count makes it hard to tell whether failures come from the test script or the system under test.

Yes. k6 can send GraphQL queries as regular HTTP POST requests with a JSON body, and it has a dedicated k6/ws module for WebSocket connections, so it isn't limited to simple REST endpoints.

Almost always staging, or a dedicated performance environment that mirrors production capacity. Running heavy load against production risks degrading the real user experience and, without careful control, can trigger outages.

k6 scripts are written in JavaScript and designed to be version-controlled and run in CI/CD like any other test code, while JMeter primarily uses an XML-based GUI test plan. k6 also uses far less memory per virtual user, since it runs a lightweight Go-based engine rather than spawning a full thread per user.

Want load tests that actually reflect production traffic?

We design realistic k6 scenarios, wire them into your pipeline, and set thresholds tuned to your real SLAs.

Book a Free Consultation โ†’ See the Service
Share
in ๐• โœ‰

Related Articles