k6: Load Testing Your APIs with JavaScript
PROGRAMMING LANGUAGES April 20, 2026, 11:30 p.m.

k6: Load Testing Your APIs with JavaScript

Load testing is a critical step in ensuring your APIs can handle real‑world traffic spikes without breaking a sweat. With k6, you get a modern, scriptable load testing tool that lets you write tests in plain JavaScript—no obscure DSLs or heavy GUIs required. In this guide, we’ll walk through setting up k6, crafting robust test scripts, and integrating the results into your CI/CD pipeline so you can ship confident, high‑performing APIs.

Installing k6 and the Basics

k6 is distributed as a single binary, making installation a breeze on any major OS. On macOS you can use Homebrew, on Windows a simple MSI installer, and on Linux a direct download or package manager works just as well. Once installed, the k6 version command confirms you’re ready to go.

At its core, a k6 script exports a default function that runs for every virtual user (VU). Think of a VU as a lightweight thread that repeatedly executes the function, sending HTTP requests and measuring latency, errors, and throughput. The http module provides all the methods you need—get, post, put, del, and more.

Your First k6 Test: A Simple GET Request

Let’s start with a minimal script that hits a public API endpoint. Save the following as basic_test.js and run it with k6 run basic_test.js. This example demonstrates the structure of a k6 script and how to log basic metrics.

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

export let options = {
    vus: 10,          // Number of virtual users
    duration: '30s', // Test duration
};

export default function () {
    let res = http.get('https://api.publicapis.org/entries');
    
    // Simple sanity check
    check(res, {
        'status is 200': (r) => r.status === 200,
        'response time < 500ms': (r) => r.timings.duration < 500,
    });
    
    // Pause between iterations to simulate think time
    sleep(1);
}

When the test finishes, k6 prints a concise summary: total requests, average response time, and any failed checks. This feedback loop is invaluable for spotting obvious performance regressions early in development.

Parameterizing Requests with Environment Variables

Hard‑coding URLs works for demos, but production tests need flexibility. k6 lets you pull values from environment variables, command‑line flags, or external JSON files. This makes the same script reusable across dev, staging, and prod environments.

Below is an enhanced version that reads the target base URL and an API key from the environment. It also demonstrates how to send a JSON payload with a POST request.

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

const BASE_URL = __ENV.BASE_URL || 'https://api.example.com';
const API_KEY = __ENV.API_KEY || 'default-key';

// Load test data from a JSON file (optional)
const users = new SharedArray('users', function () {
    return JSON.parse(open('users.json'));
});

export let options = {
    stages: [
        { duration: '1m', target: 20 }, // ramp‑up to 20 VUs
        { duration: '3m', target: 20 }, // stay at 20 VUs
        { duration: '1m', target: 0 },  // ramp‑down
    ],
    thresholds: {
        http_req_duration: ['p(95)<800'], // 95% of requests < 800ms
    },
};

export default function () {
    // Randomly pick a user for this iteration
    let user = users[Math.floor(Math.random() * users.length)];
    
    let payload = JSON.stringify({
        name: user.name,
        email: user.email,
    });
    
    let params = {
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${API_KEY}`,
        },
    };
    
    let res = http.post(`${BASE_URL}/register`, payload, params);
    
    check(res, {
        'created (201)': (r) => r.status === 201,
        'has userId': (r) => r.json('id') !== undefined,
    });
    
    sleep(0.5);
}

Run this script with k6 run -e BASE_URL=https://api.staging.example.com -e API_KEY=abcd1234 basic_test.js. The SharedArray ensures the JSON file is loaded only once per VU, keeping memory usage low even with thousands of users.

Validating Responses: Checks, Tags, and Custom Metrics

Checks are the built‑in assertion mechanism in k6. They let you verify status codes, response bodies, headers, and any custom condition you care about. When a check fails, k6 records it as a “failed request” but continues the test, giving you a holistic view of reliability under load.

Tags are key‑value pairs you attach to requests, enabling fine‑grained analysis in the output. For example, tagging requests by endpoint or HTTP method lets you slice the results later on.

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

let loginTrend = new Trend('login_response_time');

export default function () {
    // Tag the request for later filtering
    let res = http.get('https://api.example.com/v1/profile', {
        tags: { name: 'GetProfile' },
    });
    
    // Record custom metric
    loginTrend.add(res.timings.duration);
    
    check(res, {
        'status is 200': (r) => r.status === 200,
        'content-type is json': (r) => r.headers['Content-Type'] === 'application/json',
        'has username': (r) => r.json('username') !== undefined,
    });
    
    sleep(1);
}

When you export results to InfluxDB or Grafana, these tags and custom trends appear as separate series, making dashboards that highlight slow endpoints or error‑prone paths a breeze.

Designing Realistic Load Patterns

Real‑world traffic rarely follows a flat line. Users may burst during a sale, then taper off. k6’s stages configuration lets you model ramp‑up, steady‑state, and ramp‑down phases. For more complex patterns, you can use the scenarios API, which supports constant‑arrival-rate (CAR) and per‑VU iteration models.

Here’s a scenario that simulates a sudden spike of 500 requests per second for 30 seconds, followed by a graceful cool‑down.

export let options = {
    scenarios: {
        spike: {
            executor: 'constant-arrival-rate',
            rate: 500,               // 500 iterations per second
            timeUnit: '1s',
            duration: '30s',
            preAllocatedVUs: 200,    // enough VUs to sustain the rate
            maxVUs: 500,
        },
        graceful: {
            executor: 'ramping-vus',
            startVUs: 500,
            stages: [
                { duration: '1m', target: 0 }, // ramp‑down to zero
            ],
        },
    },
};

Combine this with the script from the previous section to see how your API behaves under a realistic surge.

Running Tests Locally vs. In the Cloud

For quick iterations, running k6 locally on your laptop is perfectly fine. However, local machines have limited CPU and network bandwidth, which can become a bottleneck when you need to simulate thousands of VUs. k6 Cloud (k6.io) offloads the heavy lifting to a distributed infrastructure, providing auto‑scaling, built‑in result visualisation, and easy sharing of test runs.

If you prefer an open‑source, self‑hosted solution, k6 also supports distributed execution via the k6 run --out json=out.json flag combined with external aggregators like k6 Cloud or k6 Operator for Kubernetes.

Pro tip: Start with a small local test to validate your script, then scale up using k6 Cloud for the final performance benchmark. This saves time and avoids unnecessary cloud costs during early development.

Integrating k6 into CI/CD Pipelines

Automated performance testing becomes powerful when it’s part of your CI workflow. Most CI platforms—GitHub Actions, GitLab CI, Jenkins—can run k6 as a step in the pipeline. The key is to treat performance regressions as hard failures, just like unit test failures.

Below is a minimal GitHub Actions workflow that runs a k6 smoke test on every push to main. If the 95th percentile latency exceeds 800 ms, the job fails.

name: Load Test

on:
  push:
    branches: [ main ]

jobs:
  k6-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Install k6
        run: |
          sudo apt-get update
          sudo apt-get install -y gnupg2
          curl -s https://dl.k6.io/key.gpg | sudo apt-key add -
          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 -y k6
      
      - name: Run k6 smoke test
        env:
          BASE_URL: ${{ secrets.API_BASE_URL }}
          API_KEY: ${{ secrets.API_KEY }}
        run: |
          k6 run -e BASE_URL=$BASE_URL -e API_KEY=$API_KEY smoke_test.js

For more advanced pipelines, you can export k6 results to InfluxDB, then use Grafana alerts to notify teams when performance thresholds are breached.

Real‑World Use Cases

Microservice contracts: When a new microservice is deployed, run a k6 contract test that validates both functional correctness and latency SLA across all dependent services.

API gateway throttling: Simulate burst traffic to verify that your API gateway correctly enforces rate limits without causing downstream timeouts.

CI/CD gatekeeper: Use k6 as a gate in your deployment pipeline. If the new version fails the defined performance thresholds, the deployment is automatically rolled back.

Advanced Tips & Tricks

  • Reuse data with SharedArray: Loading large CSV or JSON files once per VU saves memory and speeds up test startup.
  • Dynamic payloads: Use Math.random() or external data generators to avoid caching effects that can skew results.
  • Think time: Adding sleep() mimics real user pacing and prevents unrealistically tight request loops.
  • Custom thresholds: Define per‑endpoint thresholds to catch regressions that affect only a subset of your API surface.
  • Export formats: k6 can output results as JSON, CSV, InfluxDB line protocol, or even directly to Prometheus remote write.
Pro tip: When testing a RESTful API, group related requests (e.g., auth → data fetch → logout) in a single VU function. This models a real user session and surfaces issues that isolated endpoint tests might miss.

Monitoring and Analyzing Results

k6’s CLI summary gives you a quick snapshot, but for deep analysis you’ll want a time‑series database. InfluxDB + Grafana is the most popular stack; you can push metrics with k6 run --out influxdb=http://localhost:8086/k6. Grafana dashboards can then display request rates, error percentages, and latency percentiles over time.

If you’re using k6 Cloud, the platform automatically generates rich dashboards, provides test comparison, and even offers AI‑driven anomaly detection. Exporting results as JSON also lets you feed them into custom reporting tools or data lakes for long‑term trend analysis.

Conclusion

k6 brings the power of programmatic load testing to JavaScript developers, turning performance validation into a repeatable, version‑controlled process. By mastering script composition, realistic load patterns, and CI integration, you can catch latency regressions before they reach production and keep your APIs responsive under any load. Whether you run tests locally, on k6 Cloud, or within a Kubernetes cluster, the core concepts remain the same: write clear, data‑driven scripts, enforce meaningful thresholds, and treat performance as a first‑class quality gate.

Share this article