Roku Cloud Emulator devices
A Roku Cloud Emulator (RCE) device is a Roku that runs in the cloud instead of on your desk. The rk tool treats it as one more device kind: you register it once, then rk dev, rk ecp, rk logs, rk keysequence, rk cdp and most MCP tools reach it the same way they reach a Roku on your network. The commands that only make sense for RCE (starting and stopping a device, snapshots, and remote input) live under rk device.
RCE devices are billed while they run. Every command that starts one prints how long it may run and the most it can cost. A command that keeps running (rk dev --start with --watch or --console, or rk server after MCP device_start) stops the device it started when it exits. Anything else leaves the device running until rk device stop or its maximum runtime (see Lifecycle and cost).
Naming an RCE device
You name an RCE device with an rce: prefix wherever a command takes a device. The following table lists the selectors:
| Selector | Matches |
|---|---|
rce:<name> | The registered device with that name (case-insensitive), else, for rk device commands, the device of that name in your organization |
rce:<id> / rce-<id> | The device with that numeric RCE id |
<name> | A registered RCE device, when no LAN device has that name or id |
Only rk device commands reach a device that is not registered here, and of those rk logs tail --source firmware too. Everything that installs an app, talks to it, or starts it from an MCP session (rk dev, rk ecp, rk logs, rk device forward, rk device dev-id, device_start and the other MCP tools) needs the registration, since that is where the developer password is kept. For an unregistered device they fail and name rk device add --rce: with PACKAGE_DEVICE_NOT_FOUND from rk dev and MCP sideload_app, ECP_DEVICE_NOT_FOUND from rk ecp and MCP ecp (POST /api/ecp), and DEVICE_NOT_FOUND from the other rk server routes and MCP tools.
A bare name that matches both a LAN device and an RCE device selects the LAN one, with a warning on stderr naming the rce: spelling. As a result, registering an RCE device never takes a name away from a Roku on your network. You can use rce:<name> to select the RCE one. The command refuses a selector that matches more than one RCE device with DEVICE_SELECTOR_AMBIGUOUS; you can select one as rce:<id>. The --host option never takes an RCE device because an RCE device does not have an address of its own; you must use --device.
The rk device ls command lists both kinds, with a Kind and a Status column once an RCE device is registered. Status is running, pending (starting), shutdown, or unknown when RCE cannot be asked (no token, or the service did not answer). The rk device ls --rce command lists only the registered RCE devices, and rk device ls --org lists every device in your organization, registered here or not.
Before you start
You need an RCE organization and a personal access token from the RCE web UI. You can pick an expiry when you create it; the rk device set-rce-token command warns when the token is within 7 days of expiring.
Save the token once:
rk device set-rce-token # prompts without echoing
rk device set-rce-token < token.txt # or pipe it inThe command never takes the token as an argument; therefore, it does not end up in your shell history or process list. From a pipe, the command reads only the first line; therefore, echo "$TOKEN" | rk device set-rce-token works as well; when nothing arrives within a second, it says on stderr what it is waiting for. Surrounding whitespace is trimmed, and a token that holds anything but visible ASCII (a space, a control character, or a smart quote pasted from a document) is refused because it cannot be sent in an HTTP header. The same check applies to RK_RCE_TOKEN and to the saved file. The set-rce-token subcommand checks the token with RCE before saving and prints the user and organization it belongs to. The rk device rce-whoami command prints the same later, along with your organization's limits, and rk device clear-rce-token removes it.
The following list describes where rk keeps each file:
- The
~/roku-rce-devices.jsonfile holds the RCE devices that you register and their developer passwords. Therk device add --rcecommand writes the file, andrknever reads it from a project's.roku/directory. The~/roku-dev-config.jsonfile holds LAN devices only. Theroku-rce-devices.example.jsonfile shows one entry; it leaves out the developer password (password) on purpose, and you can set the password with--password-stdininstead of editing the file by hand. - The token is kept apart, in
~/roku-rce-config.json. Therk device set-rce-tokencommand writes it andrk device clear-rce-tokenremoves it. TheROKU_RCE_CONFIG_PATHvariable pointsrkat another token file, and the registrations, the session registry andrk server's start budget move with it: they are always kept in the token file's directory. - An unreadable token file does not hide your registrations, and unreadable registrations do not hide the token. The
rk device lscommand lists what it could read and names the file that it could not. - The token, the registrations,
roku-rce-server-starts.jsonandroku-rce-mcp-session-secret(the keyrk serversigns MCP session ids with) are written with mode0600(readable by you only). On Windows that mode does not have any effect; the files are protected only by your user profile's permissions. - The
RK_RCE_TOKENvariable, when set, takes precedence over the saved token. You can use it in continuous integration (CI), where the token comes from a masked variable. - The
~/roku-rce-sessions/directory, next to the token file, records whichrkprocesses on this machine started or are using each run. It does not hold any secrets, and an entry for a process that has exited is dropped. If that process owned the run (it was killed before it could stop it), the newestrkprocess still attached to the run owns it from then on; with none left, the run stays known as started here; therefore,rk device stopstops it without asking.
A token belongs to a user, but an organization-level token is shared by everyone who uses it: RCE, and so rk, cannot tell those people apart. A run started by a teammate with the same token shows as started by the same user, and the prompt that guards stopping someone else's run (see Stopping a device) cannot tell it is theirs.
Registering a device
The rk device add --rce command registers a device that already exists in your organization, together with the snapshot rk dev starts it from:
rk device add --rce my-tv --snapshot dev-ready --password-stdin < devpass.txt
rk device set-active rce:my-tvThe command takes the following options:
- The
--snapshot <name|id>option is required. It must be ready and must not be the device's base snapshot; a snapshot with developer mode on and the installer enabled is what makes the device usable withrk dev. - The
--password-stdinoption saves the device's developer (rokudev) password, read from stdin: a hidden prompt at a terminal, else the first line of the pipe. It is the password set on that device in Developer Settings; RCE devices do not inherit the LANdefaultPassword. - The
--firmware <id|name>and--max-runtime <seconds>options set whatrk device startandrk dev --startuse by default. - Registering does not make the device active;
rk device set-activedoes.
The rk device update rce:my-tv command changes the same settings later (--snapshot, --firmware, --max-runtime, --password-stdin), and also the device itself in RCE: --name (asks first; --yes skips the question, and without a terminal and without --yes it fails with RCE_RENAME_NOT_CONFIRMED), --note, --account-name, --property key=value (key= removes a property). It prints what it changed (the device in RCE, the registration, or both), or Nothing was changed. when you decline a rename; --json prints { device }, the registration as add --rce --json shows it. The rk device rm rce:my-tv command only forgets the registration; it never stops or changes the device in RCE.
Creating a device from scratch
RCE cannot delete devices; therefore, a device that you create counts against your organization's device quota permanently. For this reason, the rk device create command asks before it creates anything (--yes skips the question; without a terminal and without --yes, it refuses).
rk device create --rce my-tv --type tvIt prints the steps that turn the new device into one rk dev can use:
- Start the device from the snapshot
createnames, with a firmware version fromrk device firmware ls --type tv:
rk device start rce:my-tv --snapshot <snapshot> --firmware <id> - Finish guided setup with remote keys, or in the RCE web viewer:
rk device input rce:my-tv press Down Select, andrk device input rce:my-tv --text "[email protected]"for text fields.
Linking the device to an account sends an activation email; you must open the link in it within 30 minutes. - Open Developer Settings and enable the installer, setting a developer password:
rk device dev-settings rce:my-tv - Save a snapshot of the configured device:
rk device snapshot create rce:my-tv --name dev-ready --wait - Register the device with that snapshot and password:
rk device add --rce my-tv --snapshot dev-ready --password-stdin
You can stop the device when you are done (rk device stop rce:my-tv); later starts come up from dev-ready already configured.
Everyday use
The following commands show everyday use of an RCE device:
rk dev --start # Start the active device, build while it boots, install, and launch
rk dev # Build and install as usual once the device is running
rk device stop # Stop the active deviceThe --device rce:<name> option targets another registered device for one command without changing the active one.
You can also start the device on its own and leave it running across several commands:
rk device start # Start the active device and print when it stops and the most it can cost
rk dev --console
rk ecp capture-screen --output shot.png
rk device stopA command never starts a device that is not running; commands that need the device fail with a message that names rk dev --start or rk device start. Commands wait for a device that is still starting.
The rk device status [rce:<name>] command shows the device, its current run, its last snapshot and how many it has; --watch prints every state change until Ctrl+C. With a LAN device active and no device named, it says that it reports on RCE devices only and exits with code 0 (--json: { "device": null, "message": … }); naming a LAN device fails with DEVICE_KIND_UNSUPPORTED. The rk device runs command lists past and current runs with their runtime and cost (use --limit <n> for fewer).
What works on an RCE device
The following table shows which commands work on an RCE device and which work only on LAN devices:
| Works | Does not work (LAN devices only) |
|---|---|
rk dev (including --start, --console, --watch, --no-build) | rk dev --dap-debug, DAP debug sessions (--mode dap_only / unified) |
rk ecp keypress, input, query/app-ui, query/device-info, capture-screen | rk perfetto |
rk keysequence | rk device package, rk dcl |
rk logs (console) and rk logs tail --source firmware | rk device reboot, check-update, open, ip |
rk cdp (CDP-only debug sessions) | rk device lease / device pools |
rk device dev-id (always null: RCE devices are not keyed; therefore, on-device signing is not available) | MCP device_reboot, device_check_update, get_device_ip, update_device, package, dcl, perfetto_*, and debug_session_create with mode dap_only or unified |
rk device forward | |
The other MCP tools that take a device |
A LAN-only command given an RCE device fails with DEVICE_KIND_UNSUPPORTED before contacting anything. The rk tool does not include a video viewer. It drives and observes a device through keys, ECP, logs, and screenshots, and the RCE web viewer is where you watch the device.
Lifecycle and cost
RCE bills per device-hour while a device runs; rk prints cost from the list price of $0.20 per device-hour. The figure is an estimate counted from the moment the start is requested; therefore, it includes the 1 to 2 minutes that a device spends starting. You can check your actual usage with rk device usage (your own) or rk device usage --org (the organization's; owners only), narrowed with --from <date>, --to <date> and --interval 1h|24h|1w|1mo; RCE reports those values without stating a unit, and rk shows them as given.
Every run has a maximum runtime, after which RCE stops it. It is a per-run cap, not a budget: your organization sets the largest value it accepts (rk device rce-whoami shows it), and a start asking for more is lowered to it, with a note. Billing can run past the maximum runtime: RCE ends an expired run on its own schedule, which can be minutes later.
Who stops a device depends on what started it:
| Started by | Runs until |
|---|---|
rk device start | rk device stop, or its maximum runtime (--max-runtime, else the registration's, else 1 hour) |
rk dev --start | Same as rk device start (rk dev --start takes --max-runtime too), unless --watch, or --console without --auto-console-close, keeps rk dev running: then it stops when rk dev exits (Ctrl+C included), unless another rk process on this machine is still using it, which then owns the run and stops it when it exits in turn. |
rk dev --start whose build or install fails | Stopped as rk dev exits, since nothing usable was installed: right away, unless --watch or a console that connected before the failure keeps rk dev running. An install whose launch failed (LAUNCH_FAILED) counts as installed; therefore, the device keeps running as it does after a successful rk dev --start. |
MCP device_start | device_stop, 15 minutes without use, or the rk server exiting (see Server and MCP) |
| Any device someone else started | Never stopped by a command that only used it |
On Ctrl+C, rk dev waits up to 35 seconds to stop what it started, and then for any stop request it has already sent to be answered, and exits with 130 (143 for SIGTERM); a second Ctrl+C stops waiting for the release but still waits up to 20 seconds for a stop request already sent, and a third exits at once and leaves the device to its maximum runtime. Either way, exiting before the device was released says to check it with rk device status and stop it with rk device stop, and a release that fails exits 1. With --watch in a terminal, Ctrl+C also stops a device that is still booting instead of waiting for the boot to finish. When it is unclear whether RCE accepted the start, Ctrl+C watches up to 10 seconds more for the device to come up so it can stop it, and otherwise says to check with rk device status; a run that comes up started by another user, or by another rk process on this machine, or that such a process is attached to, is left running. A device that another rk process on this machine attached to while it was starting is not stopped: that process owns the run from then on. While an rk process on this machine is stopping a run, another one cannot attach to it and fails with RCE_DEVICE_NOT_RUNNING.
The rk device start command stops a run that it started when Ctrl+C reaches it before the device is up, and exits with 130 (143 for SIGTERM); a second Ctrl+C exits once any stop request already sent has been answered, and says how to check and stop the device.
When the owning process is killed without cleaning up (SIGKILL, a crash), another rk process still attached to the run takes it over (see Before you start). With none, or when the machine lost power, the maximum runtime is the only backstop. You can set the maximum runtime to the shortest time that you need.
Stopping a device
The rk device stop [rce:<name>] command stops a device and waits until RCE reports its run ended. A run that was not started from this machine (a teammate's run, one started in the web UI, or one that rk here has only used) needs confirmation: a y/N prompt at a terminal, or --yes when there is none (a script without --yes fails with RCE_STOP_NOT_CONFIRMED). The device is read again after you answer: if the run you agreed to has ended meanwhile, nothing is stopped, and if another run has replaced it, you are asked about that one. Waiting for the stop to show never stops a run started since.
The rk device command asks every confirmation (stop, create, update --name, snapshot rm) on stderr, and only when both stdin and stdout are a terminal and --json is not set; otherwise the command refuses unless you pass --yes; therefore, piped or JSON output never waits on a question that you cannot see. Ctrl+C at the question prints Cancelled. and exits with 130, changing nothing.
Snapshots
A snapshot is the saved state a device starts from. The rk device snapshot command manages them:
rk device snapshot ls rce:my-tv
rk device snapshot show rce:my-tv dev-ready
rk device snapshot create rce:my-tv --name after-login --wait # The device must be running
rk device snapshot update rce:my-tv after-login --note "signed in as test user"
rk device snapshot rm rce:my-tv after-login # Asks first; use --yes in scriptsThe rm subcommand never deletes the base snapshot, the live one, or the snapshot that a registration starts from. The rk device start --snapshot <name|id> command starts once from another snapshot without changing the registration.
Sending remote input and using Developer Settings
The rk device input command sends remote keys through RCE itself; therefore, it works before developer mode is on or ECP is enabled, which rk ecp keypress needs:
rk device input rce:my-tv press Home Down Select
rk device input rce:my-tv down Right # Hold the key
rk device input rce:my-tv up Right # Release the key
rk device input rce:my-tv --text "hello world"
rk device input rce:my-tv ready --wait # Wait until the device accepts inputThe --text option types each character as a literal key; it cannot send /. The --delay <ms> option sets the pause between keys. The rk device dev-settings command opens the Developer Settings screen.
Once developer mode is on, the ECP commands (rk ecp keypress, rk keysequence) are the faster path and match what you would use on a LAN device. If developer mode is off, or ECP is limited, they say so and name the fix: turn it on in Developer Settings and save a new snapshot.
Logs
You can read logs from an RCE device in two ways:
- The
rk logs start|get|tail|stop --device rce:my-tvandrk dev --consolecommands read the app's console (port 8085), as on a LAN device. Therk dev --consolecommand reconnects on its own if the connection drops while the device runs, and exits when the run ends. - The
rk logs tail --source firmware --device rce:my-tv [--from-start]command streams the device's firmware log: system output, not your app's console. It runs locally, withoutrk server.
Forwarding ports
The rk device forward rce:my-tv 8085 9999 command opens a 127.0.0.1 listener for each device port and holds them until Ctrl+C or until the run ends; therefore, other tools can reach the device as if it were local. For a LAN device, it prints host:port.
- RCE bridges ports 8080, 8081, 8082, 8085, 8087, 9999 and from 49152 to 65535; the command refuses any other port, including ECP's 8060 and the installer's 80, with
RCE_PORT_NOT_ALLOWEDbefore it opens anything. Therk ecpandrk devcommands reach those two through RCE on their own. - The listener does not take any credentials: while it is open, any process on this machine can reach the device through it.
- The
forwardsubcommand returns once each port accepts a connection, except 8081 (the BrightScript debug port), which is not probed, since a probe there would count as a debugger connecting; a connection to it waits for the port instead. - When the device closes a connection, the client gets what the device sent and is then closed about 2 seconds later, even if it keeps its own side open.
Screenshots
The rk ecp capture-screen command and the MCP capture_screen tool work on RCE through the device's installer rather than ECP, which RCE does not offer: an RCE device is a secure device, and its ECP does not have a screenshot endpoint. The result's source is installer. That path has the following characteristics:
- It needs the saved developer password.
- It captures only while a sideloaded app is in the foreground. On the home screen or in another app, it fails with
SCREENSHOT_NOT_OK(it retries for about 2 seconds; therefore, a capture straight after a deploy still lands). - It returns a JPEG, converted to PNG when you ask for PNG (the default).
- It returns frames whose size depends on the device, not a fixed 1920×1080.
Server and MCP
The rk server command reaches RCE devices when it has a token: started from a shell where RK_RCE_TOKEN is set, or with one saved by rk device set-rce-token (the server reads it on every request; therefore, saving one later works). Without it, the server lists RCE devices with status unknown, and the commands that only run through the server (rk logs start, rk cdp session create) refuse an RCE device and say why. Commands that can run locally fall back to your shell's token on their own.
The MCP tools that take a device accept rce:<name>, apart from the LAN-only ones in What works, which refuse it with DEVICE_KIND_UNSUPPORTED. Two tools exist only for RCE:
- The
device_starttool starts a device and makes the server its owner. The server stops it ondevice_stop, after 15 minutes without use (RK_RCE_IDLE_STOP_MINUTES,0turns it off), and whenever the server exits (rk server stop, Ctrl+C,SIGTERM,SIGHUP, or its inactivity timeout). A device counts as in use while a tool call names it, while the MCP session that started it makes any call, while a CDP or debug session is open on it, while a request reaches it through one of the server's sessions (a debug call, a log read), while another process has a session on it (rk devorrk ecprun without a server, for example), and while a command run through this server still streams from it:rk logs tail, or anrk devthe server builds. A log session the server holds open does not count on its own, since it stays open whether or not anyone reads it. Once the last of these ends, the device has the full idle time again before it is stopped. Called for a device whose stop is still under way,device_startfails withRCE_DEVICE_STOPPING. - The server also owns, and idle-stops, a run that one of its own log or CDP sessions inherited because the process that owned it exited. Such a run does not count against the two-device limit below.
- The
device_stoptool stops a device the server owns; any other device is left alone and the call namesrk device stop(RCE_NOT_SERVER_OWNED). A client keyed by itsMcp-Session-Idmay stop only what its own session started: another session's device, or a run the server inherited, is refused withRCE_NOT_SESSION_OWNEDunless the call passesforce: true. Called while that device'sdevice_startis still under way, it cancels the start, stopping the run if it began, and thedevice_startfails withRCE_START_CANCELLED. - A stop that fails leaves the device owned by the server, and still counted against its limits: the server tries again on its next check, about every 30 seconds even with the idle stop off, and at shutdown, and
device_stoptries again at once. When the stop of a run one of the server's own log or CDP sessions owned fails, the server takes that run over and retries it the same way.
The server limits RCE devices as follows: one running device per MCP session, two per server, three starts per hour for each MCP session, and six starts per hour for the whole server. The server hands each client an Mcp-Session-Id when it initializes; therefore, each editor window and each rk mcp (stdio) client is a session of its own. The id is signed with a key kept on this machine; therefore, it stays valid across server restarts and any rk server here accepts it; one no server here issued (made up or altered, from another machine, or signed with a key file since deleted or replaced) is answered with HTTP 404 and MCP_SESSION_UNKNOWN, and the client opens a new session. A client that sends no id counts as one session per address, however many connections it opens, and every such client on the machine itself shares one. The hourly cap of six starts is kept in roku-rce-server-starts.json, beside the token file; therefore, restarting the server does not reset it. The per-session counts are not kept: after a restart a session starts its count again, and a client that opens a new session gets a fresh one; therefore, the server-wide limits are the hard ones. The limits guard against an agent that keeps starting devices; they are not access control. The device_start tool runs for maxRuntimeSeconds, else the registration's --max-runtime, else 30 minutes, and never more than 2 hours. Only callers on the same machine may use these two tools. Over rk mcp (stdio) they need a running rk server; without one they fail rather than start a device nothing would stop.
The server's REST routes answer an RCE failure with 409 for a state that must change first, 404 for a device, snapshot or firmware that does not exist, 400 for a request RCE refused, 429 for RCE_RATE_LIMITED and RCE_TRIAL_LIMIT, 504 for RCE_TIMEOUT, 500 for a local RCE file that cannot be read, and 502 for anything else RCE did, a rejected token included; the body's code names the error.
Environment variables
The following table lists the environment variables that affect RCE devices:
| Variable | Effect |
|---|---|
RK_RCE_TOKEN | The RCE token; takes precedence over the saved one |
ROKU_RCE_CONFIG_PATH | Where the RCE token is kept (default ~/roku-rce-config.json); roku-rce-devices.json, roku-rce-sessions/ and roku-rce-server-starts.json are kept in the same directory; therefore, they move with it |
RK_RCE_IDLE_STOP_MINUTES | rk server: minutes without use before a device_start device is stopped (default 15, 0 off) |
RK_RCE_API_URL | Points rk at another RCE Core API (an https host under rce.roku.com, or 127.0.0.1, [::1] or localhost on any port, for testing); default https://api.rce.roku.com/api/v1. A value that is not a URL is reported by the variable's name, without echoing it. |
RK_RCE_NO_LIFECYCLE | Set to 1 by rk server in the processes it spawns: they use running devices but never start or stop one. Not meant to be set by hand. |
Troubleshooting
The following table lists common messages and codes with what to do:
| Message or code | What to do |
|---|---|
RCE_AUTH, "No RCE token is configured" | Save one with rk device set-rce-token, or set RK_RCE_TOKEN. |
RCE_DEVICE_NOT_RUNNING | Start it: rk dev --start or rk device start. |
| "Developer mode is off" | Turn it on in Developer Settings (rk device dev-settings), then save a new snapshot and register it. |
| Sideload fails with a password hint | Save the device's developer password: rk device update rce:<name> --password-stdin. |
DEVICE_KIND_UNSUPPORTED | The command supports LAN devices only (see What works). |
DEVICE_SELECTOR_AMBIGUOUS | The selector matches several RCE devices; select one as rce:<id>. |
| A warning that a name "names a LAN device and a Roku Cloud Emulator device" | The LAN device was used; select the RCE one as rce:<name>. |
SCREENSHOT_NOT_OK | Bring your sideloaded app to the foreground, for example, with rk dev. |
RCE_STOP_NOT_CONFIRMED | The run is not yours; pass --yes if you mean to stop it. |
RCE_STOP_UNCONFIRMED | The stop was sent but RCE did not report shutdown in time; check with rk device status and run rk device stop again. |
RCE_DEVELOPER_MODE_OFF | The fix is the same as for "Developer mode is off". |
RCE_ECP_LIMITED | ECP is limited or off on the device: set Settings > System > Advanced system settings > Control by mobile apps to Permissive, then save a new snapshot and register it. |
RCE_SERVER_LIMIT | rk server's per-session or per-server limit on RCE devices (see Server and MCP); stop one with device_stop first. |
RCE_START_CANCELLED | The start was cancelled before the device came up: rk device start was interrupted (Ctrl+C; it exits with 130), or device_stop was called while device_start was under way. |
RCE_NOT_SESSION_OWNED | device_stop was called from an MCP session that did not start the device; pass force: true if you mean to stop it. |
RCE_DEVICE_STOPPING | device_start was called while the server is still stopping that device; call it again once the stop is done. |
RCE_NETWORK | RCE could not be reached; the message ends with the cause (ENOTFOUND, ECONNREFUSED, a CERT_* code, …). |
A certificate error (CERT_*, SELF_SIGNED_CERT_IN_CHAIN, UNABLE_TO_VERIFY_LEAF_SIGNATURE) | A proxy or antivirus is intercepting TLS: set NODE_EXTRA_CA_CERTS to its root certificate's file. |
RCE_CONFIG_INVALID | roku-rce-devices.json or the token file cannot be read, or the token holds characters a header cannot carry; the message names the file and what to do. |
RCE_HOST_NOT_ALLOWED | RK_RCE_API_URL, or a URL RCE returned, is not one the token is sent to (see RK_RCE_API_URL under Environment variables); unset or correct the variable. |
RCE_TRIAL_LIMIT, RCE_PAYMENT_REQUIRED | Your organization's trial or billing does not allow this; check with its owner in the RCE web UI. |
Related documentation
Updated about 5 hours ago