# Runs, states and uptime

> Scheduling limits, the trial run on save, every run outcome and test state, the transition rules, how uptime is computed, which addresses Fixter will not test, and what a run detail shows.

Every HTTP and MCP test runs on a schedule, and each run ends in one outcome. This page lists
the scheduling limits, what each outcome means, how outcomes move a test between states, and
how uptime is computed from them. An agent reads runs with the `get_api_test_runs` MCP tool and
uptime with `get_uptime`; in the app, open the test from the
[API Tests page](https://app.fixter.dev/api-tests).

## Scheduling

| Setting | MCP parameter | App label | Limits | Editor default |
|---|---|---|---|---|
| Interval | `intervalSeconds` | `Check every` | At least 30 seconds | 60 seconds |
| Timeout | `timeoutSeconds` | `Give up after` | 1 to 30 seconds | 10 seconds |
| Retries | `retries` | `Retries per run` | 0 to 5 | 0 |
| Failure threshold | `failureThreshold` | `Down after` | 1 to 10 failed runs | 3 |

The timeout bounds each attempt, not the whole run. Because the timeout is at most 30 seconds
and the interval at least 30, a timeout is never longer than the interval.

A run is up to 1 + `retries` attempts. The failure threshold is how many failed runs in a row
turn the test `DOWN`, and how many rate-limited runs in a row turn it `RATE_LIMITED`.

## The trial run on save

Before a test is saved, it runs once. The trial run sends and checks exactly what the scheduled
runs will, which is why it belongs here, but it has rules of its own: it decides whether the save
happens at all, it is never retried, and it does not appear in the run history.

- **One attempt, no retries**, whatever the test's retry setting. The trial run is not written
  to the run history.
- **Saved only on a success.** A trial run that fails, errors or is rate limited saves nothing,
  and there is no way to save anyway. This holds whether the test is created enabled or paused.
- **On update**, the trial run happens only when the change affects what the test sends or checks: the
  type, the HTTP target (URL, method, headers, body, credential), the MCP target, the HTTP
  assertions, or the timeout. Changing only the name, interval, retries, failure threshold or
  the enabled flag saves without one. A failed trial run leaves the existing test unchanged.

Over MCP, a refused `create_api_test` or `update_api_test` fails with a sentence that says what
failed, starting `Not created: ` or `Not updated: ` and ending
`The test was not saved; fix its configuration and call create_api_test again.` or
`The existing test is unchanged; fix the configuration and call update_api_test again.` A
passing save returns the run as `trialRun` on the test.

In the app, the save button reads `Running test…` while the trial run is in flight. A refused
run stays on screen in the editor, headed `Trial run failed`,
`Trial run was rate limited by the target` or `Trial run could not be executed`, with its cause,
latency, failed assertions, the request and response, and a note starting
`The test was not saved.` or `Your changes were not saved.`

## Run outcomes

| Status | App label | Causes | What it means |
|---|---|---|---|
| `SUCCESS` | `Success` | None | Every assertion passed. |
| `FAILURE` | `Failure` | `ASSERTION`, `DNS`, `TLS`, `CONNECT`, `TIMEOUT`, `BLOCKED_ADDRESS` | The target is unhealthy: an assertion failed, or the target could not be reached in time. Counts toward `DOWN` and against uptime. |
| `ERROR` | `Error` | `CREDENTIAL_DECRYPT`, `UNEXPECTED`, `EXECUTOR_LOST` | A problem on Fixter's side; the test could not run. Not counted against uptime. |
| `RATE_LIMITED` | `Rate limited` | `RATE_LIMITED` | The target answered HTTP 429. Not counted against uptime. |
| `MISSED` | `Missed` | None | The run could not start in time, so the target was not probed. The row reads `Never ran`. Not counted against uptime. |

Only runs whose request reached the target, or tried to, count as executed runs: `SUCCESS`,
`FAILURE` and `RATE_LIMITED`. `ERROR` and `MISSED` runs do not count.

### Failure causes

Each failed or errored run carries one cause, shown in the app as:

| Cause | App label | When |
|---|---|---|
| `ASSERTION` | `Assertion failed` | The target answered, and one or more assertions failed. |
| `DNS` | `DNS lookup failed` | The hostname did not resolve. |
| `TLS` | `TLS handshake failed` | The TLS handshake failed. |
| `CONNECT` | `Could not connect` | The connection failed, or the exchange could not be completed. |
| `TIMEOUT` | `Timed out` | No complete response within the timeout. |
| `BLOCKED_ADDRESS` | `Private address blocked` | The target resolved to a private or internal address. See [Addresses Fixter will not test](#addresses-fixter-will-not-test). |
| `CREDENTIAL_DECRYPT` | `Credential unreadable` | The stored credential could not be read on Fixter's side. |
| `UNEXPECTED` | `Unexpected error` | The credential the test points at no longer exists (`credential deleted`), or something else went wrong while running the test (`unexpected error while running the test`). |
| `EXECUTOR_LOST` | `Executor lost` | Appears only on older runs; no new run gets it. |
| `RATE_LIMITED` | `Rate limited by the target` | The target answered HTTP 429. |

A credential's OAuth2 token fetch that fails is a `FAILURE`, not an `ERROR`; its message starts
`oauth2 token fetch: `.

## Test states

| State | App label | What it means |
|---|---|---|
| `UP` | `Up` | Passing, or not yet failing enough runs in a row to be `DOWN`. A new enabled test starts here. |
| `DOWN` | `Down · <n> fails` | The failure threshold of failed runs in a row was reached. The label carries the current streak, for example `Down · 3 fails`. |
| `RATE_LIMITED` | `Rate limited` | The failure threshold of rate-limited runs in a row was reached. The app shows it as `Rate limited`. |
| `PAUSED` | `Paused` | Not running on its schedule; no new runs are recorded. |

A test created disabled starts `PAUSED`. In the app, `Pause test` and `Resume test` on the
test's page switch it; `disable_api_test` and `enable_api_test` do the same over MCP. Resuming
returns the test to `UP` with both streaks cleared.

## How states change

A test keeps two streaks: failed runs in a row and rate-limited runs in a row. Each run moves
them as follows. `ERROR` and `MISSED` runs move nothing, and pausing parks a test in `PAUSED`
from any state until it is enabled again, which starts it at `UP`.

- A `SUCCESS` clears both streaks and returns a `DOWN` or `RATE_LIMITED` test to `UP`.
- A `FAILURE` adds to the failure streak and clears the rate-limit streak. When the failure
  streak reaches the failure threshold, an `UP` or `RATE_LIMITED` test turns `DOWN`.
- A `RATE_LIMITED` run adds to the rate-limit streak. When it reaches the failure threshold, an
  `UP` or `DOWN` test turns `RATE_LIMITED`. With a threshold of 1, the first 429 turns the test
  `Rate limited`.
- An `ERROR` or `MISSED` run changes neither streak, so it neither breaks a run of failures nor
  adds to one.
- A `PAUSED` test does not change state.

## Uptime

Uptime over a window is:

```
uptime = SUCCESS / (SUCCESS + FAILURE)
```

`ERROR`, `MISSED` and `RATE_LIMITED` runs are left out of both terms, because they say nothing
about whether your endpoint was up. A window with no `SUCCESS` or `FAILURE` run has no uptime
(the app shows `-`), not zero. Latency percentiles also count `SUCCESS` and `FAILURE` runs only.

A downtime interval is an unbroken streak of `FAILURE` runs, from the first failing run's
scheduled time to the last one's finish. Only `SUCCESS` and `FAILURE` runs shape it: an
`ERROR`, `MISSED` or `RATE_LIMITED` run in the middle neither ends a downtime nor starts one,
so a rate-limited episode is never downtime.

Ask your agent for a test's uptime and it calls `get_uptime`, which covers the last 24 hours
unless given a window. In the app, the test's `Uptime and latency` card shows `Uptime`,
`p95 latency` and `Runs` with a count per outcome, and the latency chart marks downtime as
bands.

<!-- fx-image: an API test page with the Uptime and latency card and downtime bands on the chart -->

## Addresses Fixter will not test

Fixter's tests reach public addresses only:

- **Public DNS only.** Hostnames resolve through public DNS resolvers, never through an internal
  resolver, a hosts file or search domains. An internal name therefore does not resolve and fails
  as an ordinary `DNS` failure.
- **No private or internal addresses.** If any address a hostname resolves to is private,
  loopback, link-local, cloud metadata, shared address space, documentation, benchmarking,
  multicast or reserved, in IPv4 or IPv6, the connection is refused. An IP address written in
  the URL is judged the same way. The run fails with cause `BLOCKED_ADDRESS` and the
  message `the URL points at a private or internal address, which the monitor does not probe`.
- **Redirects and OAuth2 token URLs too.** Every connection passes the same check, each
  redirect hop included, and so does a credential's OAuth2 token URL. A blocked token URL fails
  the run as `BLOCKED_ADDRESS` with the message prefixed `oauth2 token fetch: `.
- **No allowlist.** There is no per-account exception.

A `BLOCKED_ADDRESS` run is a `FAILURE`: it is retried like any failure and counts toward
`DOWN`. A trial run refused this way says, over MCP,
`Not created: the URL points at a private or internal address, which the monitor does not probe. Use a URL that is reachable from the public internet.`

## Retries and Retry-After

A run retries on any failure: a failed assertion, a timeout, or a connect, DNS, TLS or
blocked-address failure, up to `retries` more attempts. It stops at the first attempt that is
not a failure. A 429 is never retried, so a rate-limited run is one attempt whatever the retry
setting. The app shows `Took <n> attempts` on a run that needed more than one.

On a 429 Fixter reads the target's `Retry-After` header, as seconds or as an HTTP date. A
zero, negative, unreadable or missing value defers nothing, and the value is capped at one hour.
The next run moves to the rate-limited run's finish time plus that `Retry-After`, but only when
that is later than the next normal run; a 30 second `Retry-After` on a five minute test defers
nothing. Runs skipped by a deferral are not recorded as `MISSED`.

The run message says which happened:

- `Rate limited by the target (HTTP 429)` when nothing was deferred.
- `Rate limited by the target (HTTP 429); next check deferred by <N> seconds`, or
  `... by <N> minutes`, when the deferral moved the next run. The duration is how far past the
  next normal run it moved, in seconds under 120 seconds and in whole minutes from there.

An HTTP test whose status pattern accepts 429 (for example `429` or `4xx`) treats a 429 as an
ordinary response, and none of this applies. See [HTTP tests](/api-testing/http-tests/). For
letting the tests through a rate limiter, see
[Allow tests through your firewall](/api-testing/firewalls/).

## What a run records

### One trace id per run

Every run gets one W3C trace id. Each attempt sends it to your endpoint in a `traceparent`
header (same trace id across a run's retries, new span id per attempt), and
[`{{traceId}}`](/api-testing/request-variables/) is the same id. A service that propagates
incoming trace context, which OpenTelemetry SDKs do by default, continues that trace instead of
opening its own, so the spans and logs it records for the request carry Fixter's trace id. If the
service reports to Fixter, that is what links the run to them. To keep your own ids, set a
`traceparent` request header on an HTTP test; a header you set yourself replaces Fixter's.

### What your agent gets

`get_api_test_runs` returns each run's outcome, timing, failed assertions, `traceId` and
`retryAfterSeconds`; `correlate` on the `traceId` finds what happened inside your service. The
request and response themselves are not returned over MCP; they are in the app.

### What the app shows

Open a run on the test's page:

- The run message and the failed assertions.
- The trace id, with `Open in Logs` (logs for that trace around the run's time) and
  `View trace`.
- `Request`: the method, URL, headers and body of the run's last attempt. For an MCP test this
  is the last request of the session, not counting the closing session `DELETE`.
- `Response`: the status, `first byte` and `total` time, headers and body, or
  `The target never answered.`

<!-- fx-image: an expanded run on an API test page showing the trace links, request and response -->

### What is redacted or cut

`Authorization`, `Proxy-Authorization`, `Cookie`, `X-Api-Key` and `Api-Key` are always shown as
`<redacted>`, even when typed into the test's own headers, and so is every header and query
parameter a credential adds. The request body is shown as sent. Bodies are cut at 16 KB; a cut
response body says `Body cut at 16 KB. The target returned more.`