> ## Documentation Index
> Fetch the complete documentation index at: https://modelrunner.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Request lifecycle

> How an inference request moves from submission to terminal state, and the platform guarantees around finalization.

## States

Every request progresses through one of these statuses:

```text theme={null}
IN_QUEUE  ──►  IN_PROGRESS  ──►  COMPLETED
                            ──►  FAILED
                            ──►  CANCELLED
```

| Status        | Meaning                                                                     |
| ------------- | --------------------------------------------------------------------------- |
| `IN_QUEUE`    | The request is accepted and waiting for the provider to start processing.   |
| `IN_PROGRESS` | The provider is actively generating output.                                 |
| `COMPLETED`   | Output is available at `response_url`. Billing has been settled.            |
| `FAILED`      | The request was force-failed by the platform after being stuck. Not billed. |
| `CANCELLED`   | Reserved. **Not currently reachable** — see below.                          |

`COMPLETED`, `FAILED`, and `CANCELLED` are **terminal** — the request will never transition away from them.

<Note>
  Two things about terminal states that are easy to get wrong:

  **A failed generation is recorded as `COMPLETED`, not `FAILED`.** When the provider errors, the request itself completed — it just produced no output — so it lands on `COMPLETED` with a populated `error` and a `billingStatus` of `failed`. `FAILED` is reserved for requests the platform force-fails after they get stuck. So **`status` alone does not tell you whether the work succeeded**; check `billingStatus` (`charged` or `partial` means real output).

  **`CANCELLED` is never written today.** Nothing sets it, and `cancel_url` does not cancel (see [Cancellation](#cancellation)). Handle it defensively if you like, but do not build a flow that waits for it.
</Note>

## Submitting a request

```bash theme={null}
POST /{ownerName}/{modelName}
Authorization: Key $MODELRUNNER_KEY
Content-Type: application/json

{ "prompt": "two friends cooking together" }
```

The response returns immediately with `request_id`, `status: "IN_QUEUE"`, and three URLs:

| URL            | Use                                                                                                                 |
| -------------- | ------------------------------------------------------------------------------------------------------------------- |
| `status_url`   | Poll for status transitions (or skip polling — see below).                                                          |
| `response_url` | `GET` once status is `COMPLETED` to retrieve the validated output.                                                  |
| `cancel_url`   | `GET` (yes, GET). Returns the current status — it does **not** currently cancel. See [Cancellation](#cancellation). |

## Tagging requests with metadata

The submit body may include a reserved top-level `metadata` object alongside the model's input fields — a flat string map of your own tags (job ids, environments, batch labels):

```bash theme={null}
POST /{ownerName}/{modelName}
Authorization: Key $MODELRUNNER_KEY
Content-Type: application/json

{
  "prompt": "two friends cooking together",
  "metadata": { "env": "prod", "batch": "42" }
}
```

Metadata is stored on the request and returned whenever you read the request back — `GET /requests/{requestId}`, the `response_url` result payload, and list items. (The lightweight `status_url` envelope does not include it.) It is **never sent to the model** and never merged into the stored `input`.

Limits (violations return `400`):

| Constraint | Value                         |
| ---------- | ----------------------------- |
| Max keys   | 16                            |
| Key length | 1–64 characters               |
| Values     | strings only, ≤512 characters |

Filter your request history by metadata with a single URL-encoded JSON query param — pairs match exactly and are AND-ed, combinable with `status`, `modelEndpoint`, and pagination:

```bash theme={null}
curl -G https://queue.modelrunner.run/requests \
  -H "Authorization: Key $MODELRUNNER_KEY" \
  --data-urlencode 'metadata={"env":"prod","batch":"42"}' \
  -d status=COMPLETED -d limit=10
```

<Note>
  `metadata` is a reserved word at the top level of the submit body: a model whose own input schema defines a `metadata` field cannot receive it through the raw body.
</Note>

## Three ways to watch a request

<AccordionGroup>
  <Accordion title="1. Server-Sent Events (recommended for UIs)">
    Open one connection to `GET /requests/stream` and receive push updates for every in-flight request the user owns. On connect, the server emits a `snapshot` event with current state; thereafter each status transition emits an `update` event. A `: hb` SSE comment is sent every 25 seconds to keep the connection alive — your client can ignore it.

    This eliminates the polling loop entirely. Best for dashboards, multi-request UIs, and any client that opens multiple requests in parallel.
  </Accordion>

  <Accordion title="2. Webhooks (recommended for server-to-server)">
    Pass a `webhook` URL in the submit body and we POST the result to it when the request settles — no polling loop and no long-lived connection, so nothing is lost if your process restarts mid-request.

    ```json theme={null}
    { "prompt": "…", "webhook": "https://example.com/hooks/modelrunner" }
    ```

    Deliveries are signed with [Standard Webhooks](https://www.standardwebhooks.com) HMAC-SHA256, retried for about two hours, and inspectable after the fact. See [**Webhooks**](/docs/guides/webhooks) for the payload, verification snippets and the retry schedule.
  </Accordion>

  <Accordion title="3. Polling">
    `GET /{ownerName}/{modelName}/requests/{requestId}/status` returns the same response shape as the create call. Poll at 1–2 second intervals. The SDK helpers (`subscribe` in JS, `submit_async` + `iter_events` in Python) wrap this loop for you.

    <Warning>
      The `queue_position` field is currently always `0` — real queue depth is not tracked. Don't surface it as "you're #N in line" in your UI.
    </Warning>
  </Accordion>
</AccordionGroup>

## Cancellation

`GET /{ownerName}/{modelName}/requests/{requestId}/cancel` returns the request's current status payload.

<Warning>
  **This endpoint does not currently cancel anything.** It is wired to the same handler as `status_url` and returns the same response, so the request continues to run and will still be billed if it completes. No request ever reaches `CANCELLED` (see the note under [States](#states)).

  The route exists so that clients and SDKs built against it keep working once real cancellation ships. Until then, treat a submitted request as uninterruptible and do not rely on `cancel_url` to stop billable work.
</Warning>

## Platform safety net: automatic finalization

You do not need to implement retry logic for requests you've abandoned. A background sweep runs every **60 seconds** and re-checks every request that is:

* Still `IN_PROGRESS` past the provider's expected duration, or
* Marked `COMPLETED` by the provider but missing output media (media upload still in flight).

The sweep advances each row through its terminal state automatically (uploads media to S3, generates thumbnails, charges billing on success, or marks failed otherwise).

A request that has been retried **5 times** and is older than **6 hours** is force-failed. After that point its status is permanently `FAILED` and any associated billing is reverted.

<Tip>
  Concretely: if your client crashes between submitting a request and polling for its result, the platform will still finalize the request correctly. You can retrieve it later with `GET /requests/{requestId}` (no model path required) or via [`list_my_requests`](/docs/api-reference/list-requests).
</Tip>

## Billing tie-in

Billing settles on the same lifecycle:

* A request transitioning to `COMPLETED` charges the user's balance.
* A request transitioning to `FAILED` or `CANCELLED` is **not** charged.
* A request that completed at the provider but failed schema validation is recorded with `billingStatus: "failed"` — you are not billed, but the call returns `422` so you can surface the upstream error.

See [errors](/docs/api-reference/errors) for the `422` payload shape.
