Which responses does k6 count as failed in http_req_failed, and how do you change that rule?
answer
- k6 judges every response's status
- 200 to 399 is the default band
- expectedStatuses builds the rule object
- setResponseCallback, or responseCallback in params
- null removes tag and metric together
basics
~10 sk6 treats statuses 200 through 399 inclusive as expected; anything else, including a transport error reported as status 0, is counted as failed. Change the rule with http.setResponseCallback(http.expectedStatuses(...)) or a per-request responseCallback param.
solid answer
~40 sEvery request k6 makes is run past a **response callback**. The default accepts statuses `200`-`399` inclusive; anything outside that -- a `404`, a `500`, or a transport failure that k6 reports as status `0` -- is counted as failed. The verdict is written twice: as the `expected_response` tag on the request's samples, and as a `http_req_failed` sample carrying the inverse. You replace the rule with `http.setResponseCallback(http.expectedStatuses(200, 201, { min: 400, max: 404 }))`, which applies to every later request in that VU, including the next iteration. For one request only, pass the same object as `responseCallback` in that request's params. `http.setResponseCallback(null)` switches the whole mechanism off: no `expected_response` tag and no `http_req_failed` sample at all.
code
javascript · 14 linesimport http from 'k6/http';
// Module scope: every VU starts with this rule.
http.setResponseCallback(http.expectedStatuses(200, 201, { min: 400, max: 404 }));
const only201 = http.expectedStatuses(201);
export default function () {
http.get('https://quickpizza.grafana.com/api/status/404'); // expected
http.post('https://quickpizza.grafana.com/api/post', '{}', {
headers: { 'Content-Type': 'application/json' },
responseCallback: only201,
});
}go deeper
Remember the default band: k6 treats statuses 200 to 399 as expected, and everything else feeds the failure rate. You are not expected to reconfigure it yet, only to know it exists.
Explain the mechanics: one callback per response, an expected_response tag plus an inverse http_req_failed sample, and the three scopes -- module-wide via setResponseCallback, per-request via responseCallback, or off with null.
Show judgment about redirect chains and transport errors. Demonstrate that narrowing the rule globally to satisfy one endpoint reclassifies redirects elsewhere, and that a VU-scoped callback survives into later iterations.
Weigh a single script-wide rule against per-endpoint callbacks. One rule is easy to reason about but wrong for any endpoint with legitimate 4xx traffic; per-endpoint rules are accurate but scatter the definition of failure across the codebase.
## What the response callback is k6 does not decide on its own that a `404` is a failure. Every response is passed to a **response callback**, a small object that answers one question: was this status expected? The answer drives two things and only two things. - The `expected_response` tag is attached to the request's samples with the value `"true"` or `"false"`. - One `http_req_failed` sample is emitted per request, carrying the **inverse**: `0` when the response was expected, `1` when it was not. Nothing else changes. The script does not throw, the iteration does not stop, and the process exit code is untouched. `http_req_failed` is a rate you observe, not a control-flow event. ## The default rule Out of the box the callback accepts **statuses 200 through 399 inclusive**. That range is wider than most people expect, and the consequences are worth stating plainly: - A `304 Not Modified` is expected. A `301` redirect is expected. - A `404` and a `500` are **not** expected and each contributes a `1` to `http_req_failed`. - A request that never got a response at all -- DNS failure, refused connection, timeout -- is evaluated as status `0`, which falls outside `200`-`399`, so it counts as failed too. - Every hop of a redirect chain is evaluated separately. If you narrow the rule to only `200` and the endpoint answers `301` before `200`, the `301` hop is recorded as failed. ## Building your own rule `http.expectedStatuses(...)` builds the callback object. It takes any number of arguments in any order, and each argument is either an integer status or an object of the form `{ min, max }` giving an inclusive range. ```javascript const okOrNotFound = http.expectedStatuses(200, 201, { min: 400, max: 404 }); ``` Non-integers are rejected loudly: `http.expectedStatuses(200, '300')` throws `argument number 2 to expectedStatuses was neither an integer nor an object like {min:100, max:329}`, and a fractional bound such as `{ min: 200, max: 300.5 }` is rejected the same way. Calling it with no arguments at all throws. ## Where you install it There are three scopes, and the difference between them matters. | How you set it | What it covers | When it takes effect | |---|---|---| | `http.setResponseCallback(cb)` | every later request made by that VU | immediately, and it persists into the VU's next iteration | | `responseCallback: cb` in a request's params | that single request | for that call only | | `http.setResponseCallback(null)` | every later request by that VU | disables the tag and the metric entirely | The persistence of `setResponseCallback` across iterations is a real trap. The `k6/http` module instance belongs to the VU, not to the iteration, so a callback set halfway through the default function is still in force when that VU starts its next iteration. If you want a narrowed rule to apply for the whole run, call `setResponseCallback` once at module scope, outside the default function, so every VU starts from the same state. ## Building the request rule you actually want 1. Decide which statuses your endpoint legitimately returns under load -- an idempotent GET that answers `404` for a missing record may be perfectly healthy. 2. Express that set with `http.expectedStatuses`, using `{ min, max }` for contiguous ranges and bare integers for one-offs. 3. Install it at module scope for a whole-script rule, or as `responseCallback` in params for the one endpoint that differs. 4. Leave the default alone for everything else -- narrowing globally to satisfy one endpoint quietly reclassifies every redirect in the script. Two documented exceptions exist because of how the auth flows work: with `auth: 'digest'` the first response is always expected to be a `401`, and with `auth: 'ntlm'` a `401` on the first request is accepted in addition to whatever your callback allows. ## When to disable it `http.setResponseCallback(null)` removes both the `expected_response` tag and the `http_req_failed` emission for that VU. It is a deliberate opt-out for a script that judges responses entirely through its own assertions, not a way to make failures go away -- with the metric absent, nothing downstream can read a failure rate at all. All of this is k6 v2.x behaviour.
- A k6 run reports a non-zero http_req_failed rate but every check passed. What happened?The two mechanisms are independent. `http_req_failed` reflects only the response callback's verdict on the status, while checks evaluate whatever predicates you wrote. A `404` your assertions deliberately tolerate still lands as a `1` in `http_req_failed` unless you widen the callback with `http.expectedStatuses` to include it.
- Why can narrowing k6's response callback to a single status inflate the failure rate?The callback runs on every hop, not just the final response. If you accept only `200` and the endpoint answers `301` before `200`, the redirect hop is evaluated as unexpected and emits its own failed sample, so one logical request contributes two samples, one of them failed.
saying these in an interview costs you the question
- Thinking only 2xx statuses are expected by default in k6
- Believing a failed http_req_failed sample aborts the iteration
- Expecting setResponseCallback to reset at the end of each iteration
- Assuming a connection error is skipped rather than counted as status 0
- Passing a plain arrow function to setResponseCallback instead of expectedStatuses