---
title: "How it works"
description: "What one install is made of, how the duty cycle works, and what wakes it."
---

> Documentation Index
> Fetch the complete documentation index at: https://oc-workers.mdick85.workers.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# How it works

## One install

One install on your account is:

- one Worker, the front door;
- one Control Durable Object (Control DO), which holds config, the message inbox, alerts and the record of every operation;
- one Runtime DO for each epoch. An epoch is one image plus one Worker code version, and one epoch is active at a time. The Runtime DO owns the container;
- one container, 2 vCPU, 6 GiB memory, 20 GB disk by default, that runs OpenClaw;
- one sandbox container for agent tools, only when `sandbox` is on;
- one R2 bucket, `oc-host-<inst>` by default.

The Worker is `oc-host` by default, at `https://<name>.<subdomain>.workers.dev`. With [Cloudflare Access](https://oc-workers.mdick85.workers.dev/docs/security/#cloudflare-access) on, the URL is your custom domain.

Inside the container, a supervisor (ocsup) restores state, starts OpenClaw and runs the sleep steps. It reaches the Worker through gates, private addresses that the Worker serves ([Security](https://oc-workers.mdick85.workers.dev/docs/security/#what-the-container-can-reach) lists them).

## The owner console

The owner console is the page at your install URL (`/`), the same on a custom domain and on workers.dev. Without the owner cookie, it shows the login form and nothing else.

With the owner cookie, it shows a state card and three tabs. The top bar has the install name, **Open Control UI** (`/chat`) and **Sign out**. The state card shows the state (running, asleep, waking, retrying) and for how long, the last activity, the next scheduled wake, the projected month cost at the last 24 hours' pace, the check count, and one state button:

| State | Button |
|---|---|
| Asleep | **Wake now** |
| Starting | **Waking, 20 s**, disabled |
| Running | **Sleep now**. When a rule keeps the install awake, the rule shows beside the button. |
| Going to sleep | **Going to sleep**, disabled. There is no cancel. |
| Backoff, or every restore refused | **Retry now**, with the fix line of the open alert beside it |
| Stopped by you | **Start** |

Each tab is its own page, and the tab names are links: Overview is `/`, Alerts and log is `/?tab=log`, and Setup is `/?tab=setup`. An item shows in one tab only, and buttons are only in Setup and Alerts and log.

| Tab | What it holds |
|---|---|
| Overview | Cost: this month at the last 24 hours' pace, the always-on month, percent saved, and the cost guard. An alerts summary that links to Alerts and log. Budgets: each daily cap with a bar, and **Raise** when one is at 80 percent. Connected checks: model (provider, model id, gateway, last test time), web search, browser and web fetch, media, each channel (transport, webhook, last platform error, whether it blocks sleep), the install (CLI, image, OpenClaw, upgrade available), config render, wake list and held inbox. Backups: newest archive age and verified flag, snapshots and chain length, replica lag, next maintenance. |
| Alerts and log | Open alerts first: severity badge, detail, fix line, and **Acknowledge** (closes the alert). For `doctor-failed`, it also shows the tail of the Doctor output. Then the log of wakes, sleeps, restarts and archives of the last 30 days, newest first, with the time and how long each took. Filter the log by kind with the links above the table. Then closed alerts. |
| Setup | Things to connect and set up, one row each with its state and one button, one control, or the command to run: Telegram, pairing requests with **Approve**, other channels, the alert channel (a URL field with **Save**, **Test** and **Off**), Access (a command: it needs a Cloudflare API token on your computer), the sandbox (a list of off, on and all, with **Apply**), the model (a list of the models in the config, with **Apply**), an upgrade or config render when one is due, and a daily limit near its cap with **Raise**. It also has **Archive now** and **Restore drill**. |

Notes:

- **Approve** does the same as `ochost pairing approve telegram <code>`. **Raise** doubles a limit, up to 1,000,000,000. A second click on the same page does not double it again.
- The three controls run the same `/ops` actions as the CLI. **Apply** on the sandbox saves the choice in the Control DO and restarts the install, with no deploy. The row says "Applies at the next start" until the next boot. **Apply** on the model posts a new config version (source `owner`) with `agents.defaults.model.primary` changed, then restarts. If the install sleeps, it does not wake: the change applies at the next start.
- **Save** on the alert channel stores the URL in the Control DO. The Worker cannot set its own secret, so the sender uses this URL first and the `ALERT_URL` secret second. **Off** stops all alerts, even when the secret is set. **Test** sends the same test alert as `ochost alerts test` and shows its code; run `ochost alerts ack <code>` to confirm it arrived. `ochost alerts setup` still works: it sets the secret, which applies when no URL is saved in the console.
- A choice made in the console wins over the value in `oc-host.jsonc`. After you use the sandbox control, `ochost sandbox on|off` and a deploy no longer change the sandbox, so keep using the console.
- A status is a dot and a word: OK, Warning or Failing. A warning or failing row has the fix line that `ochost doctor` prints.
- After a button, the page returns to the tab that holds the button.
- The page follows the light or dark setting of your system.
- Opening the page does not wake the install (see [Security](https://oc-workers.mdick85.workers.dev/docs/security/#the-owner-console)). The model test and the Telegram webhook check run at most once a minute. While the install sleeps, check rows show the last values and say so.
- `GET /ops/doctor` serves the same JSON, and `ochost doctor` prints it, so the page and the CLI agree.
- The page works with JavaScript off. Each tab is a link and each button is a form. A small script reloads the current tab every 30 seconds (5 seconds while the state changes).

## The duty cycle

The duty cycle is the default mode, `sleep.mode: auto`: the container sleeps when it is idle and wakes when something needs it.

| Setting | Default | Meaning |
|---|---|---|
| `sleep.idleMinutes` | 5 | Idle time before sleep. It doubles after a quick re-wake, up to 60. |
| `sleep.minAwakeMinutes` | 2 | Stay awake at least this long after a boot. |
| `sleep.minSleepMinutes` | 10 | The next known wake must be at least this far away. |
| `sleep.uiIdleMinutes` | 15 | An open Control UI tab is closed after this long without user input. |
| `sleep.uiMaxMinutes` | 60 | A tab is closed after this long without a chat message or a write (a change made from the UI). |
| `sleep.maxAwakeHours` | 16 | Past this uptime, the install restarts when idle. |
| `sleep.proactive` | `2h@08-22` | OpenClaw's proactive check runs every 2 hours from 08:00 to 22:00 local time. |

These keys are in `oc-host.jsonc`. Change one with `ochost config set <key> <value>`, and it applies at the next op, with no deploy.

To sleep, the install also needs an empty inbox and no running turn, cron run or pending delivery. A cost guard keeps the install awake when the last 24 hours of sleep and wake cycles would cost more than always-on.

A sleep takes a snapshot of the container disk, and a wake starts the container from that snapshot. Each wake also gets a new generation (gen) of the SQLite replica, a new R2 path that only the current boot may write. This stops a container from an earlier boot from writing over newer data.

## What wakes it

| Event | Wakes? |
|---|---|
| A Telegram message from a paired sender | Yes. The Worker stores and acknowledges it first, then the container starts and receives it. |
| A verified Slack (HTTP mode) or Twilio SMS event | Yes. Same path as Telegram. There is no wake list for these yet. Twilio delivery receipts do not wake. |
| A cron alarm (OpenClaw cron jobs and the proactive check) | Yes. The Control DO sets a wake alarm before the next job. |
| You open the Control UI (`/chat`) with the owner cookie | Yes. You see a waking page until the container runs. |
| You select **Wake now** in the owner console | Yes. |
| You select **Archive now** in the owner console | Yes. The archive runs in the container. |
| An `ochost` command that needs the container, such as `wake` or `backup create` | Yes. |
| Daily maintenance | Yes, if there was no wake in 24 hours or a held message is 20 hours old. |

## What does not wake it

| Event | Result |
|---|---|
| An anonymous request, a wrong path or a wrong webhook secret | 404 or 401. No wake. |
| You open the owner console at `/` | No. It shows the state and cached checks. |
| A Telegram message from an unknown sender | Stored and acknowledged. Held until the next wake. No wake. |
| A Control UI tab that was closed for idle | The tab shows "offline". Its reconnect does not wake the install. Reload the page to clear it. |

## Cold reply time

In tests:

- The first reply after a cold wake took p50 31.6 s with a mock model (30 runs).
- From the acknowledgement of a message to a running container took p50 32.9 s (200 sleep and wake cycles).
- One real reply came 20.8 s after the message. The model took 3.5 s of that.

While Telegram waits, the Worker shows a "Waking up" message (see [Channels](https://oc-workers.mdick85.workers.dev/docs/channels/#the-waking-message)), and replies stream by message edits. A warm-up call at boot exists but is off, because it made the first reply slower in tests.

A warm reply takes about 1 second plus model time. [Limits and tradeoffs](https://oc-workers.mdick85.workers.dev/docs/limits-and-tradeoffs/) states the bound as "about a minute".

The Worker acknowledges the Telegram webhook at the edge while the container sleeps. In 200 sleep and wake cycles the acknowledgement took p50 72 ms and p99 390 ms. No update was lost.

## What keeps it awake

A channel that needs a socket keeps the install always-on. The sleep check reads the live channel status, and a running channel account that does not use the managed webhook blocks sleep. Other blockers are a running cron job, an open ingress event, a pending delivery, an event-driven job and an open `ochost import` (it fails with `import-stalled` after 30 minutes without an answer). `ochost status` names the blocker. See [Channels](https://oc-workers.mdick85.workers.dev/docs/channels/) and [Cost](https://oc-workers.mdick85.workers.dev/docs/cost/).

## Design choices, and the failures they prevent

oc-workers started from a reading of the earlier Cloudflare template for OpenClaw, [cloudflare/moltworker](https://github.com/cloudflare/moltworker), and the failures its users reported. Four of those reports shaped the design.

| Reported failure | Cause in the template | oc-workers | Tested |
|---|---|---|---|
| Stuck at "Container is starting up". | The start runs inside a web request. No supervisor, no time limit, no retry. | The Runtime DO owns the start. Each attempt has a time limit (30 s, 300 s on the first pull of a new image). A failed start is a step on the restart ladder, which waits longer before each new try and never gives up: up to 5 minutes between tries for a platform error, and every 30 minutes with a high alert every 6 hours at its last level. A start that goes 40 rounds without a running container ends with `start-loop` and goes to the ladder too. The console shows the state and the fix. | 50 random kills, 0 restart timeouts. |
| Ran for days, then the start never ends. | The backup expires after 7 days and is deleted. A restore can run again under a live gateway. | No expiry. An old archive is removed only when a newer one is verified, and the newest verified archive is always kept. The restore runs before the gateway starts, never under it. | No empty boot in the loss tests. Rebuilt after a wiped Control DO in 69.3 s. |
| The gateway runs, no reply comes. | No inbox. A message lost in a restart is gone. | The Control DO stores and acks every message before it wakes the container. A message accepted and not answered before a start is replayed once. A message not answered for 5 minutes while the gateway runs raises the alert `no-reply`. | 587 messages during 50 kills, 586 acked; 4 unanswered led to the replay rule. 15 of 15 answered once after a kill right after accept. |
| Data loss, R2 mount failures. | State lives on a mounted bucket. A failed mount means an empty home and a fresh setup. | No mount. The database streams to R2 through a gate; the container holds no R2 key. Archives are verified by read-back and a monthly restore drill. An install with state refuses to boot empty. | See [State and backups](https://oc-workers.mdick85.workers.dev/docs/state-and-backups/). |

## Platform reliability

A 72 hour test on Cloudflare with 24 injected faults showed 0 platform-caused container stops in 3.7 awake instance-days, and the sleep-mode instance had 71 of 71 wakes succeed. After a fault we caused, the container restarted in 224 to 1045 s, of which 180 to 240 s is the restart delay we set. Restore time grew from 18 to 41 s to 215 to 249 s in the last two restarts. Cloudflare gives no uptime guarantee for containers.

## Next

Go to [Limits and tradeoffs](https://oc-workers.mdick85.workers.dev/docs/limits-and-tradeoffs/).

Source: https://oc-workers.mdick85.workers.dev/docs/how-it-works/index.mdx
