K6's HTTP module covers every method you will need for REST API load testing. This lesson walks through each one — GET, POST, PUT, PATCH, DELETE — plus batch requests and request tagging. All of them return the same response object, and learning to read it efficiently saves time across every test you write.
The HTTP methods
GET — reading resources
import http from 'k6/http';
export default function () {
// Simple GET
const res = http.get('https://api.test.com/users');
// GET with query parameters built into the URL
const filtered = http.get('https://api.test.com/users?role=admin&active=true');
// GET with headers
const authRes = http.get('https://api.test.com/protected', {
headers: { Authorization: 'Bearer mytoken' },
});
}POST — creating resources
const payload = JSON.stringify({
name: 'Alice',
email: 'alice@test.com',
role: 'admin',
});
const params = {
headers: { 'Content-Type': 'application/json' },
};
const res = http.post('https://api.test.com/users', payload, params);Three things to remember every time you POST JSON: serialise with JSON.stringify(), set Content-Type: application/json, and pass both as the second and third arguments. K6 will not serialise plain objects for you.
PUT and PATCH — updating resources
const updated = JSON.stringify({ name: 'Alicia', role: 'superadmin' });
const params = { headers: { 'Content-Type': 'application/json' } };
// PUT replaces the entire resource
http.put('https://api.test.com/users/1', updated, params);
// PATCH applies a partial update
http.patch('https://api.test.com/users/1', updated, params);The function signatures are identical to POST: URL, body, params.
DELETE — removing resources
// DELETE usually has no body
http.del('https://api.test.com/users/1');
// DELETE with auth
http.del('https://api.test.com/users/1', null, {
headers: { Authorization: `Bearer ${token}` },
});Note the method name is http.del, not http.delete — delete is a reserved keyword in JavaScript.
The request lifecycle — what K6 measures
http_req_duration is the total time from sending the request to receiving the last byte. When it is high, compare the breakdown: if http_req_waiting dominates, the server is slow to process; if http_req_receiving dominates, the response payload is large; if http_req_blocked dominates, the connection pool is saturated.
Reading the response object
Every HTTP method returns the same response object:
const res = http.post('https://api.test.com/users', payload, params);
console.log(res.status); // 201
console.log(res.body); // '{"id":42,"name":"Alice"}'
console.log(res.json()); // { id: 42, name: 'Alice' }
console.log(res.json('name')); // 'Alice' — JSONPath-like extraction
console.log(res.json('data.user.email')); // nested field access
console.log(res.headers['Content-Type']); // 'application/json'
console.log(res.timings.duration); // total time in ms
console.log(res.timings.waiting); // TTFB in msres.json('fieldName') extracts a field from a JSON body without a separate parse call. It supports dot notation for nested fields.
Batch requests — parallel within a VU
A single VU can send multiple requests in parallel using http.batch(). This models page loads that trigger several simultaneous API calls:
const responses = http.batch([
['GET', 'https://api.test.com/users'],
['GET', 'https://api.test.com/products'],
['GET', 'https://api.test.com/orders'],
]);
const [usersRes, productsRes, ordersRes] = responses;All three requests are sent in parallel. Without http.batch() they would be sequential — tripling the time a VU spends waiting per iteration. Use batch when your system would naturally serve these requests simultaneously (a dashboard page loading multiple widgets, for example).
Tagging requests for metric filtering
By default, all requests contribute to a single http_req_duration metric. Tags split this by endpoint so you can apply per-endpoint thresholds:
http.get('https://api.test.com/users', { tags: { name: 'GetUsers' } });
http.post('https://api.test.com/orders', payload, {
headers: { 'Content-Type': 'application/json' },
tags: { name: 'CreateOrder' },
});The output and Grafana dashboards then show separate metric rows:
http_req_duration{name:GetUsers}.....: p(95)=120ms
http_req_duration{name:CreateOrder}..: p(95)=340msYou can apply separate thresholds to each:
thresholds: {
'http_req_duration{name:GetUsers}': ['p(95)<200'],
'http_req_duration{name:CreateOrder}': ['p(95)<800'],
},⚠️ Common mistakes
- Passing a plain object as the POST body.
http.post(url, { name: 'Alice' })sends[object Object]as the body string. Always callJSON.stringify()first and setContent-Type: application/json. - Using
http.delete()instead ofhttp.del().deleteis a reserved JavaScript keyword. K6 exposes the method ashttp.del(). Writinghttp.delete()throws a syntax error. - Forgetting tags in multi-endpoint tests. Without tags, all requests contribute to one aggregated average — making it impossible to see which endpoint is causing a slow p(95). Tag every request with a meaningful
namefrom the start.
🎯 Practice task
Build a CRUD test against a public REST API. 30 minutes.
Use https://jsonplaceholder.typicode.com — free, no auth required.
- Write a K6 script with
vus: 3, duration: '30s'that performs a CRUD cycle per iteration:GET /posts/1— check status 200 andres.json('title')is a non-empty string.POST /postswithJSON.stringify({ title: 'Test', body: 'K6 test', userId: 1 })— check status 201.PUT /posts/1with an updated payload — check status 200.DELETE /posts/1— check status 200.
- Tag each request with
{ tags: { name: 'OperationName' } }. - Run and confirm you see separate metric rows for each tagged endpoint in the output.
- Add an
http.batch()call that fetches/posts/1,/users/1, and/albums/1in parallel. Compareiteration_duration p(95)with and without batching.