Checks are the primary way k6 tells you when your load test is actually failing, not just when it’s running slowly. They’re assertions that run during the test, and if any check fails, the test is marked as failed.

Here’s a k6 script with a check:

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

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

export default function () {
    const res = http.get('https://httpbin.org/get');

    check(res, {
        'status is 200': (r) => r.status === 200,
        'response body contains "origin"': (r) => r.body.includes('"origin"'),
    });
}

When this script runs, k6 will execute the http.get request. Then, for each response res, it will run the two checks defined. The first checks if the HTTP status code is exactly 200. The second checks if the string "origin" is present anywhere within the response body. If either of these conditions is false for any request, the check fails, and k6 will report a test failure at the end.

Think of checks as the guardrails for your load test. Without them, a test could run for hours, consuming resources, and return a "success" status even if the application is returning 500 errors or malformed data. Checks transform a simple performance benchmark into a functional validation under load. They allow you to verify not just that your system is fast, but also that it’s returning correct and expected results.

Internally, k6 collects the results of all checks executed throughout the test. At the end of the test run, it aggregates these results. If any check failed, even just once, the overall test status will be marked as a failure. This is crucial for CI/CD pipelines, where a load test failure should stop the deployment process. You can also configure thresholds based on check failures. For example, you could fail the test if more than 1% of checks fail, even if the requests themselves are fast.

The check() function takes the result of an operation (like an HTTP response object) as its first argument, and then one or more objects defining the checks. Each check object has a name (a string) and a callback function that returns a boolean. The callback receives the operation’s result as its argument. If the callback returns true, the check passes; if it returns false, the check fails.

You can also use k6 checks to validate specific data points within a JSON response. For example, if you’re expecting a user object with a specific ID, you could do something like this:

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

export default function () {
    const res = http.get('https://api.example.com/users/123');
    const body = res.json(); // Parse the JSON response

    check(res, {
        'user ID is correct': (r) => body.id === 123,
        'user name is present': (r) => body.name !== undefined,
    });
}

This demonstrates how checks are not limited to simple status codes. They are powerful tools for asserting the correctness of your application’s behavior under stress.

The most surprising thing about k6 checks is how granular you can get with them, and how they can be used to validate complex business logic, not just HTTP responses. For instance, you can check the content of a specific field in a deeply nested JSON object, or verify that a sequence of operations resulted in a particular state change. This moves load testing beyond simple performance metrics into true functional validation under load.

Consider a scenario where you’re testing an e-commerce checkout process. A single http.post might represent the final order submission. However, your checks could verify:

  1. The HTTP status is 201 Created.
  2. The response JSON contains an order_id.
  3. The order_id is a valid UUID format.
  4. The total_amount in the response matches the sum of items in the request.
  5. A specific status field in the response is "processing".

This level of detail means your load test isn’t just telling you if the checkout API is fast; it’s telling you if it’s actually working correctly for every single simulated user.

The check function is designed to be lightweight. It doesn’t introduce significant overhead to your test execution, making it suitable for use on every iteration of your load test. This is in contrast to some end-to-end testing frameworks where assertions might be performed on a much smaller subset of requests due to performance implications.

When you define multiple checks for a single request, k6 reports the success or failure of each individual check. This provides rich diagnostic information. If a response passes the overall check call but one of its internal assertions failed, you’ll see exactly which assertion failed, making debugging much easier.

You might be tempted to put all your assertions into a single, very long boolean expression within one check. While technically possible, it’s much better practice to break down complex assertions into multiple, individually named checks. This dramatically improves the readability of your test results and makes it far easier to pinpoint the exact condition that failed. For example, instead of check(res, { 'all conditions met': (r) => r.status === 200 && r.body.includes('success') && r.timings.duration < 500 }), you should use three separate checks: 'status is 200', 'response body includes success', and 'duration is less than 500ms'.

The next step after mastering basic checks is understanding how to use k6’s threshold system in conjunction with checks to define sophisticated pass/fail criteria for your entire test run.

Want structured learning?

Take the full K6 course →