# Stream lifecycle

> The five statuses a stream moves through, the timers that move it, and why a stream ends.

A stream is one broadcast. Its key works once: when the stream is over, create a new one for the next broadcast.

## Statuses

```text
            create
              │
          ┌───▼────┐   30 min, nothing connected   ┌─────────┐
          │pending │──────────────────────────────▶│ expired │
          └───┬────┘                                └─────────┘
              │ publisher accepted      DELETE     ┌───────────┐
              │            └──────────────────────▶│ cancelled │
          ┌───▼────┐                                └───────────┘
          │  live  │
          └───┬────┘
              │ DELETE, publisher gone 60 s, max duration,
              │ a limit broken, or credit at zero
          ┌───▼────┐
          │ ended  │
          └────────┘
```

| Status | Meaning | Can go live? |
|---|---|---|
| `pending` | Created; capacity reserved; waiting for the publisher. | Yes, for 30 minutes |
| `live` | A publisher is connected (or dropped less than 60 s ago). | It is |
| `ended` | It was live and is over. The recording is being made. | No |
| `expired` | Nothing connected within 30 minutes. | No |
| `cancelled` | Deleted before it went live. | No |

## The timers

| Timer | Length | Where to see it |
|---|---|---|
| Waiting for the publisher | 30 minutes from creation | `reservation_expires_at`, `reservation_expires_in_seconds` |
| Reconnecting after a drop | 60 seconds, same key | `status` stays `live` meanwhile |
| Maximum duration | 4 hours (new accounts), 24 hours (trusted) | `max_duration_seconds` |
| Ingest limit warning | 20 seconds before the stream is ended | `ingest.limit_warning` |

There is no way to reserve a stream for a future time. Create it when the publisher is about to start.

## Why a stream ended

`end_reason` on an ended, expired or cancelled stream:

| `end_reason` | What happened | What to do |
|---|---|---|
| `deleted` | You called `DELETE /v1/streams/{id}`. | — |
| `disconnected` | The publisher left and didn't come back within 60 s. | Normal end for OBS "Stop Streaming". |
| `duration_limit` | It reached `max_duration_seconds`. | Create a new stream to continue. |
| `reservation_ttl` | Nothing connected within 30 minutes. | Create the stream closer to the start. |
| `limit_bitrate` | Over 7.5 Mbps arrived for 20 seconds after the warning. | Set the encoder to at most 6000 Kbps. |
| `limit_resolution` | Video larger than `max_resolution` for 20 seconds after the warning. | Scale the output down, or create the stream with a higher `max_resolution`. |
| `insufficient_credit` | Your credit reached zero. | Top up, then create a new stream. |
| `simulcast_not_supported` | A browser sent several video layers. | Publish one video track. |
| `provision_failed` | No ingest server could be started. Rare. | Create a new stream. |

## What is arriving

While live, `ingest` shows what the ingest server measures, updated every few seconds:

```json
"ingest": {
  "width": 1280,
  "height": 720,
  "kbps": 2710,
  "observed_at": "2026-09-29T10:32:14.120Z",
  "limit_warning": null,
  "keyframe_interval_seconds": 2,
  "keyframe_warning": false
}
```

`kbps` counts everything received, audio and overhead included, so a 2500 Kbps video setting reads a little higher. `keyframe_warning` is only advice: the stream keeps running.

## Following a stream

Add a [webhook](/webhooks) and we tell your server when the stream goes live, ends and has its recording. Or poll `GET /v1/streams/{id}` every 3–5 seconds while you care, and stop when the status is `ended`, `expired` or `cancelled`. The player page does its own polling through the CDN, so viewers add no load to your API calls.

The same read has `viewers`: how many are watching now (from the hosted player), and the peak and total watch time (exact, from the CDN logs, about an hour later). See [Hosted player](/player#viewer-counts).
