How to Implement API Load Testing with k6 in Node.js (2026 Guide)
Load testing is one of those things every developer knows they should do but often skips because the tooling feels overwhelming. You've built an amazing API in Node.js, deployed it to production, and then—disaster. Your API crashes under real traffic because you never tested how it performs under load.
In this guide, I'll show you how to use k6, the modern load testing tool that's become the go-to choice for developers in 2026. We'll cover writing realistic test scripts, defining performance thresholds, and integrating load testing into your CI/CD pipeline.
What is k6 and Why Should You Care?
k6 is an open-source load testing tool developed by Grafana (yes, the same company behind Grafana observability). Unlike traditional tools that require steep learning curves, k6 lets you write tests in plain JavaScript. If you know how to write a Node.js API, you already know how to write k6 tests.
Here's why developers love k6 in 2026:
- Developer-friendly API: Write tests in JavaScript, not some proprietary scripting language
- Built-in checks and thresholds: Define pass/fail criteria that actually matter
- CI/CD integration: Run load tests as part of your deployment pipeline
- Grafana integration: Visualize results in dashboards you already use
- Cloud and local execution: Test locally or scale to hundreds of thousands of virtual users in the cloud
Setting Up k6 for API Testing
First, install k6 on your machine. On macOS, it's a simple Homebrew install:
brew install k6
On Linux, use the official script:
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
For Windows, download the installer from the k6 website.
Writing Your First API Load Test
Let's create a test script for a typical REST API. We'll test an endpoint that returns a list of resources.
Create a file called api-test.js:
// api-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
// Test configuration
export const options = {
// Virtual users - how many concurrent users
vus: 20,
// Test duration
duration: '1m',
// Performance thresholds - these define pass/fail criteria
thresholds: {
// 95th percentile response time must be under 500ms
http_req_duration: ['p(95)<500'],
// 99th percentile must be under 1 second
'http_req_duration{expected_response:true}': ['p(99)<1000'],
// Error rate must be under 1%
http_req_failed: ['rate<0.01'],
// 95% of checks must pass
checks: ['rate>0.95'],
// Minimum throughput of 10 requests per second
http_reqs: ['rate>10'],
},
};
// This function runs for each virtual user
default function () {
// Make a GET request to your API
const baseUrl = 'https://your-api.example.com';
const response = http.get(`${baseUrl}/api/v1/resources`);
// Check that the response is successful
check(response, {
'status is 200': (r) => r.status === 200,
'response time OK': (r) => r.timings.duration < 400,
'has correct content-type': (r) => r.headers['Content-Type'].includes('application/json'),
});
// Simulate realistic user behavior with sleep
sleep(0.5);
}
Testing Multiple API Endpoints with Scenarios
Real APIs have multiple endpoints with different traffic patterns. In 2026, k6's scenario feature lets you model complex traffic patterns accurately.
// multi-endpoint-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
// Custom metrics
const errorRate = new Rate('errors');
const apiDuration = new Trend('api_duration');
export const options = {
scenarios: {
// Smoke test - light load to verify basic functionality
smoke: {
executor: 'constant-vus',
vus: 5,
duration: '30s',
tags: { test_type: 'smoke' },
},
// Load test - normal expected traffic
load: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 50 }, // Ramp up to 50 users
{ duration: '5m', target: 50 }, // Hold at 50 users
{ duration: '2m', target: 0 }, // Ramp down
],
tags: { test_type: 'load' },
},
// Stress test - push beyond normal capacity
stress: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '1m', target: 100 },
{ duration: '2m', target: 200 },
{ duration: '2m', target: 300 },
{ duration: '1m', target: 0 },
],
tags: { test_type: 'stress' },
},
},
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'],
http_req_failed: ['rate<0.01'],
errors: ['rate<0.1'],
},
};
const baseUrl = 'https://your-api.example.com';
// Test different endpoints
export default function () {
// Test GET /api/v1/resources
const getResponse = http.get(`${baseUrl}/api/v1/resources`);
check(getResponse, {
'GET resources - status 200': (r) => r.status === 200,
'GET resources - has data': (r) => r.json('data') !== undefined,
});
errorRate.add(getResponse.status !== 200);
apiDuration.add(getResponse.timings.duration);
sleep(0.3);
// Test POST /api/v1/resources (create new resource)
const createPayload = JSON.stringify({
name: 'Test Resource',
type: 'example',
});
const postResponse = http.post(`${baseUrl}/api/v1/resources`, createPayload, {
headers: { 'Content-Type': 'application/json' },
});
check(postResponse, {
'POST resource - status 201': (r) => r.status === 201,
'POST resource - has id': (r) => r.json('id') !== undefined,
});
errorRate.add(postResponse.status !== 201);
apiDuration.add(postResponse.timings.duration);
sleep(0.3);
// Test GET /api/v1/users (authentication endpoint)
const authResponse = http.get(`${baseUrl}/api/v1/users/me`, {
headers: { 'Authorization': `Bearer ${__ENV.API_TOKEN}` },
});
check(authResponse, {
'GET user - status 200': (r) => r.status === 200,
});
errorRate.add(authResponse.status !== 200);
}
Testing API Authentication
Most production APIs require authentication. Here's how to test authenticated endpoints:
// authenticated-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { RefinedSession } from 'k6/http';
export const options = {
vus: 10,
duration: '1m',
};
const baseUrl = 'https://your-api.example.com';
// Get access token once per virtual user
export function setup() {
const loginResponse = http.post(`${baseUrl}/api/v1/auth/login`,
JSON.stringify({
email: 'test@example.com',
password: 'testpassword123',
}),
{
headers: { 'Content-Type': 'application/json' },
}
);
const token = loginResponse.json('access_token');
return { token };
}
// Use the token in requests
default function (data) {
const headers = {
'Authorization': `Bearer ${data.token}`,
'Content-Type': 'application/json',
};
// Test authenticated endpoint
const response = http.get(`${baseUrl}/api/v1/profile`, { headers });
check(response, {
'authenticated request succeeds': (r) => r.status === 200,
'response has user data': (r) => r.json('email') !== undefined,
});
sleep(0.5);
}
Running k6 Tests
Execute your tests with the k6 CLI:
# Run locally with output to terminal
k6 run api-test.js
# Run with cloud results (requires k6 account)
k6 run --out cloud api-test.js
# Run with environment variables
k6 run -e API_TOKEN=your_token_here authenticated-test.js
# Run specific scenario
k6 run --env TEST_TYPE=load multi-endpoint-test.js
Integrating k6 into CI/CD
The real power of load testing comes from running it automatically. Here's how to integrate k6 into your GitHub Actions workflow:
# .github/workflows/load-test.yml
name: API Load Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup k6
uses: grafana/k6-action@v0.2.0
with:
version: 'latest'
- name: Run smoke test
run: k6 run smoke-test.js
env:
API_URL: ${{ secrets.API_URL }}
API_TOKEN: ${{ secrets.API_TOKEN }}
- name: Run load test (on main branch only)
if: github.ref == 'refs/heads/main'
run: k6 run load-test.js
env:
API_URL: ${{ secrets.API_URL }}
API_TOKEN: ${{ secrets.API_TOKEN }}
Analyzing k6 Results
When k6 completes, you'll see a summary like this:
✓ status is 200
✓ response time OK
✓ has correct content-type
checks.........................: 95.00% ✓ 2850 ✗ 150
http_req_blocked...............: avg=5.23ms min=0s med=2ms max=98ms p(95)=15ms
http_req_connecting............: avg=3.12ms min=0s med=1ms max=45ms p(95)=8ms
http_req_duration.............: avg=125ms min=80ms med=110ms max=450ms p(95)=250ms
http_req_failed................: 0.50% ✓ 15 ✗ 1485
http_reqs......................: 25/sec min=0 med=25 max=35 p(95)=32
Key metrics to watch:
- http_req_duration: How long requests take (p95 and p99 matter most)
- http_req_failed: Percentage of failed requests
- http_reqs: Throughput (requests per second)
- checks: Percentage of custom assertions that passed
Best Practices for API Load Testing in 2026
- Test in production-like environments: Your staging environment should mirror production closely
- Use realistic data: Test with data volumes similar to production
- Test incrementally: Start with smoke tests, then load tests, then stress tests
- Set meaningful thresholds: Don't just guess—baseline your API's normal performance
- Monitor during tests: Watch your API's metrics (CPU, memory, database connections) while testing
- Test regularly: Run load tests after significant code changes
- Use k6 Cloud for scale: For realistic high-load testing, k6 Cloud can generate millions of requests
Conclusion
Load testing doesn't have to be complicated. With k6, you can write tests in JavaScript that are easy to understand, maintain, and integrate into your development workflow. The key is starting simple—test your critical endpoints first, establish baselines, and gradually add more complex scenarios.
Remember: it's better to catch performance issues in testing than in production. Your users will thank you.
Ready to level up your API testing? 1xAPI provides reliable, scalable APIs for developers. Check out our API marketplace for production-ready APIs you can integrate into your applications today.
Top comments (0)