Runtime
The runtime
The runtime is the local process that boots devices, holds sessions and serves every operation over REST, RPC, MCP and the stream on one loopback port. This page covers starting and stopping it, what it keeps on disk, how it protects your machine, and how to clean up.
Daemon or in process
The runtime runs in one of two ways.
- As a daemon.
troupe test,troupe install,troupe studioandtroupe mcpstart it in the background when none answers. It stays up between commands, so a device from the last run is still warm for the next. It exits by itself after 30 minutes with no session and no device. - In process.
troupe test --no-daemon, or any run withCIset, runs the runtime inside the test command and shuts its devices down at the end. See Running in CI.
Only one runtime serves a $TROUPE_HOME at a time. A --no-daemon run refuses to start while a daemon runs
for the same home.
Start, stop and status
Start the daemon yourself when you want to call it from the client or REST:
bunx troupe runtime start --detachStarted the Troupe runtime at http://127.0.0.1:<port> (pid <pid>).Without --detach it runs in the foreground, logs to the terminal and stops on Ctrl+C. A detached daemon
writes its output to $TROUPE_HOME/logs/daemon.log.
bunx troupe runtime statusTroupe runtime <version> at http://127.0.0.1:<port> (pid <pid>), up since <time>
sessions: 1 live
device <name> (ios-simulator): leased, <session id>
governor: normal, <n> MB availableA device is creating, booting, ready (warm, with no session), leased (held by a session),
shuttingDown or broken. --json prints the same as JSON: the output of
runtime.status with running, url and pid added. The
command exits 1 when no daemon runs.
bunx troupe runtime stopstop refuses while sessions are live. --force ends them, with the reason runtime-shutdown, and shuts
their devices down.
What it serves
The daemon binds 127.0.0.1 only, on a port it takes from 47000 to 47999.
| Path | What |
|---|---|
/v1/<group>/<operation> | REST, one route per procedure |
/rpc/v2/<group>/<operation> | RPC |
/mcp | MCP over Streamable HTTP |
/v1/stream | The live stream, a WebSocket |
/ | Troupe Studio |
Every request passes one gate. The Host header must name the loopback address, an Origin must be the
runtime's own or a paired one, and every procedure except runtime.version and pairings.redeem needs a
bearer token. Calling the API directly has the details.
What it keeps on disk
Everything is under $TROUPE_HOME, which defaults to ~/.troupe and is readable by you alone.
| Path | What |
|---|---|
runtime.json | The running daemon's url, its owner token, its pid and its contract version. Written when the daemon listens, removed when it stops |
daemon.lock | Held for the runtime's whole life, so two runtimes never share a home |
host.json | Host settings: toolchain paths and limits. troupe install writes it |
logs/daemon.log | A detached daemon's output |
logs/runtime.jsonl | The runtime's structured log, with secrets redacted |
state/devices.json | The registry of devices Troupe created |
state/processes.json | The child processes the runtime started |
cache/ | What is built once, such as the XCUITest runner |
The owner token in runtime.json holds every scope. Treat it as a password.
It is new each time the daemon starts, so read the file when you connect instead of copying the token.
A second directory, ~/.troupe-host, holds a ledger of the ports and homes that all of your runtimes use.
Set TROUPE_HOST_HOME to move it.
Host safety
The runtime works on a machine you also use for other things. These rules hold whatever a caller asks.
- It touches only what it made. Troupe creates simulators and AVDs named
troupe-…, records each in its registry, and acts only on registered devices. Anything else is reported at most, never changed. - It stays on loopback. Every listener binds
127.0.0.1on a port from Troupe's range. - It limits devices. At most two devices run per platform by default. Set
platforms.<id>.maxDevicesinhost.jsonto change that. - It watches memory. A governor samples memory pressure. Under pressure it shuts down warm devices first.
At the
criticallevel it refuses new sessions, and after 30 seconds there it stops Troupe's newest device; that session endserroredwith the reasonhost-pressure. - It refuses instead of queueing. When no device slot or memory is free,
sessions.createfails withCAPACITY_UNAVAILABLE, a reason (busyorhost-pressure) andretryAfterMs.troupe testwaits and retries. - It keeps released devices warm, briefly. A device released healthy has its apps terminated and stays booted for 5 minutes for the next session with the same spec, with its apps still installed. A warm device gives way before a new session is refused.
- It cleans up after itself. Each child process runs in its own process group and is recorded. A starting runtime first removes what an earlier one left. A small watcher process, the reaper, does the same 60 seconds after a runtime dies without shutting down, unless another runtime took the home over.
host.json
troupe install writes this file the first time it runs:
{
"version": 1,
"profile": "developer",
"platforms": {
"ios-simulator": { "developerDir": "/Applications/Xcode.app/Contents/Developer" }
}
}The file is strict: an unknown key stops the CLI with the file and the key in the message. Besides each
platform's keys (iOS, Android,
Catalyst), a troupe object takes:
| Key | Default | What it does |
|---|---|---|
portRange | [47000, 47999] | The TCP ports Troupe may bind |
memoryBudgetMb | none | A ceiling on the memory of all Troupe devices together |
reserveForProductionMb | 2048 | Free memory the governor keeps for everything that is not Troupe |
warmPoolTtlMs | 300000 | How long a released device stays warm, at most the default. 0 turns the warm pool off |
The developer profile is for your own machine. A shared profile exists for a Mac that also runs other
workloads: it reserves their ports and device names and tightens the budgets.
Cleaning up
Normally there is nothing to clean. Two commands exist for when a crash, a forced kill or a power loss left something behind.
bunx troupe runtime sweepsweep lists leaked devices, AVDs and process groups that Troupe owns, one per line with what it would do
(remove or report). Anything it cannot prove is Troupe's is listed as such and never touched. Add
--apply to remove what is listed for removal.
- Through a running daemon, devices are only reported, because a live session may hold one.
--applystops stray process groups. - With no daemon, or with
--no-daemon, the sweep runs in the command itself, as a starting runtime sweeps. There--applydeletes leaked devices too.
bunx troupe runtime kill-allkill-all stops everything Troupe owns for your user, whether or not a daemon answers. It stops every
running runtime, then sweeps every $TROUPE_HOME a runtime of yours has used. --disable also sets
enabled: false for every platform in host.json, so nothing starts again until you change it back.
Both commands exit 1 when something Troupe owns is still there afterwards.
Next
- Sessions: how a device is acquired, kept and released
- Troupe Studio: watch and drive the daemon's devices
- The
troupeCLI: everyruntimesubcommand and option