# Changelog What shipped in Runtime Cloud, newest first. Runtime ships every day. Each entry is something you can use or see; the guides hold the current terms, and [pricing](./pricing) holds the current rates. Each day, and each entry, also has a page of its own at [withruntime.com/changelog](https://withruntime.com/changelog), and every entry arrives in the [RSS feed](https://withruntime.com/changelog/feed.xml). ## 9 November 2026 - **A sandbox says what you can do with it now.** Every sandbox read from `/v1/sandboxes` and `/v2/sandboxes` returns `actions`: the actions that change it from its current state (`pause`, `wake`, `stop`, `restart` and `delete`). A lifecycle action that cannot apply, such as a pause of a starting sandbox, still returns `409`, and its `details` now carry `state` and the same `actions` list; its hint is written for that state. A wait for a state (`?waitFor=running`) now returns immediately once the sandbox cannot get there without a new action, such as a sandbox you stopped, instead of after its whole timeout. Every SDK reads the list: `sandbox.can("pause")` in TypeScript and Python, `Can("pause")` in Go, `can("pause")` in Java and `can?("pause")` in Ruby. The states and every move between them are on [Sandbox states and actions](/docs/sandbox-lifecycle). ## 8 November 2026 - **A `503 busy` says whether the cell could not confirm the request or could not complete it.** Before, every `503 busy` from `/v1/sandboxes` read "The cell could not confirm this in time; it may or may not have been applied.", whatever the cause. That message now comes only when the cell's log or a timeout is the reason. Any other cause, such as a sandbox's disk or its guest not answering, returns "The cell could not complete this right now; it may or may not have been applied." The status, the `busy` code and `details.reason` are unchanged. Retry with the same `Idempotency-Key`. ## 7 November 2026 - **Desktops: capture the screen with `POST /v2/desktops/{id}:screenshot` and drive it with `POST /v2/desktops/{id}:act`.** A screenshot returns the whole screen as base64, as `png` or `jpeg` (set `quality`, 1 to 100), with its width and height. An action moves, clicks, drags or scrolls the mouse by pixel, types text, presses xdotool key names such as `ctrl+l` or `Return`, reads the cursor position, lists the windows and focuses one, opens a URL in the desktop's Chromium, or launches a program. Both run inside the desktop's own sandbox. Returns `400` (`invalid_request`) when a field is outside its limits, `409` (`desktop_not_running`) when the desktop is not running, and `409` (`desktop_failed`) with the command's own error when it fails. Available in every SDK as `desktops.screenshot` and `desktops.act`. - **Apps, Functions and Containers: volumes are mounted.** A version whose `spec.volumes` names a volume now mounts it in each copy before its process starts. Before, the version failed with `deploy_build_failed: volumes cannot be read here yet`. A volume that is still being created is waited for; one that does not exist, or was deleted after the request was accepted, fails the version with `not_found`. A `POST`, `PUT` or `PATCH` that sets `spec.volumes` needs `volumes:attach`; without it, the request returns 403. - **Sandboxes: mount a volume through v2.** `POST /v2/sandboxes` accepts `spec.volumes`, a list of at most one `{volume, path}` entry naming a volume of the same project by its ID (`vol_...`). The volume is mounted read-write at its `path` before the sandbox's phase is `running`; a volume that is still being created is waited for. The sandbox writes the volume alone: while it holds it, no other sandbox mounts it, and it cannot be forked. The API key needs `volumes:attach`, and must be able to read the volume: without `volumes:attach` the request returns 403, and a volume the key cannot read, or that does not exist, returns 400. `volumes` is set when the sandbox is created and cannot be changed. Each `path` must be absolute, outside `/proc`, `/sys` and `/dev`, and neither at nor inside a file share's path; otherwise the request returns 400. ## 5 November 2026 - **Status incidents in the API.** `GET /v2/status/incidents` returns the incident notes shown on withruntime.com/status, `status.md` and `status.json`, newest start first, as `{"data": [...]}`. It requires no API key. Each incident has `id`, `title`, `start`, `end` (`null` while ongoing), `saw` and `fixed`; a published note is never rewritten, and later updates are appended to `fixed`. The status page now reads these notes live, so a new or updated incident appears on the page's next load. - **Per-address limit on creating organizations.** `POST /v2/orgs`, which creates a signed-in person's own organization, is limited to 10 an hour per client IP address (an IPv6 address counts by its /64). A request over the limit returns `429 rate_limited` with a `Retry-After` header giving the seconds until one more request is accepted; the refused request does not count toward the limit. - **Closing an account deletes its resources.** `POST /v1/account:close` now succeeds for every account; for some accounts it previously returned `500 internal_error`. From the response on, requests with the account's API keys to sandboxes, volumes and the other resource APIs return `401`, and the account's resources are deleted within seconds. Data-bearing resources, such as volumes and snapshots, are held for 7 days before they are purged. The response's `released` field is `false` while that deletion is in progress. - **Cloud Run: Google's client libraries' `$alt` parameter is accepted.** The Google Cloud Run Admin API at `/compat/google-cloud-run` now accepts the `$alt` query parameter (`json`, or `json;enum-encoding=int`), which Google's Node.js client (`@google-cloud/run`) sends with every REST request. Previously each such request returned 400. Responses are JSON, with enum values as names, which the clients read. Any other `$alt` value returns 400. - **Seven more launch checks through competitors' APIs on /status.json.** `launch_checks` on `GET https://withruntime.com/status.json` now also returns `dev_environments_codespaces` (Octokit, GitHub's SDK, against the Codespaces API), `containers_fly` (Fly's Machines API), `containers_cloud_run` (Google's Cloud Run client for Node.js), `firewall_security_groups` (the AWS SDK for JavaScript's EC2 client), `uptime_checks_betterstack` (Better Stack's monitors API), `uptime_checks_checkly` (Checkly's checks API) and `sites_vercel` (Vercel's SDK, promoting a release). Each entry creates, uses, reads back and deletes one resource through Runtime's compatible API, with the same fields as the other launch checks. For a product billed per resource (`dev_environments_codespaces`), the entry also confirms that the resource's usage was billed, and its `steps_ms` includes `usage`. The other six products are not billed per resource, so their entries have no `usage` step. - **`GET /v1/me` returns the key's reach.** The response now includes `reach`: `agent` when the key acts only on what its own agent made, `compartment` when it acts on what every key of its compartment made, or `account` when it acts on everything in the account. The value reflects the key as the API treats it at the time of the request, so an account-wide key whose maker is no longer an owner or admin returns `agent`. Every SDK returns it from `me()`. - **A build whose identical image is larger than its `maxImageMiB` fails with the size to set.** When your account already had an identical image larger than a new build's `build.maxImageMiB` (8192 by default), the build failed with "The image could not be saved because of a fault on our side". It now fails immediately with `An identical image of this organization is N MiB, larger than this build's build.maxImageMiB of M. Set build.maxImageMiB to at least N.` Set `build.maxImageMiB` to at least that size and the build copies the identical image immediately. - **Per-address limits on sign-in and API key creation.** Requests that sign in or create an account are limited per client IP address (an IPv6 address counts by its /64): 30 an hour for `POST /v2/sign-in/email`, and 30 an hour for `POST /v2/sign-in/email/verify` and Google or SSO sign-in returns together. Creating or rotating API keys (`POST /v2/keys`, `POST /v1/keys`) is limited to 60 an hour. Each API server applies these limits on its own. A request over a limit returns `429 rate_limited` with a `Retry-After` header giving the seconds until one more request is accepted; nothing is created, and the refused request does not count toward the limit. Accounts that already exist count the same as new ones. - **Clearer error when a sandbox from an image cannot start.** When the server could not prepare a sandbox's image, `POST /v1/sandboxes` returned `503 busy` with "The cell could not confirm this in time; it may or may not have been applied." It now returns `503 busy` with "This sandbox could not be started on its server, so nothing was made." Retry the create with the same `Idempotency-Key`. - **Render environment variable endpoints for Apps.** `/compat/render/v1` now serves Render's environment variable endpoints over an App's `spec.env`: list (`GET /services/{serviceId}/env-vars`, paginated by `cursor` and `limit`), replace all (`PUT /services/{serviceId}/env-vars`), get, set and delete one (`GET`, `PUT` and `DELETE /services/{serviceId}/env-vars/{envVarKey}`). Setting or deleting one variable leaves the others unchanged. Getting or deleting a variable the service doesn't have returns 404. `generateValue` returns 400. - **Sandboxes, Apps and Functions can call App and Function addresses.** A request from a sandbox, an App or a Function to an App's or a Function's address (`https://.runtimehost.com`) used to return `403` with "Destination resolves to a forbidden address". These addresses are now reachable over HTTPS on port 443, as they are from the internet. Private address ranges, Runtime's own servers, and other ports on these names are still refused. - **Render list pagination reads only the requested page.** On `/compat/render/v1`, `GET /services` and `GET /services/{serviceId}/deploys` now start reading from the position the `cursor` names and stop as soon as the page is full. Previously each request read the whole list first. Without a `name` or `status` filter, a request reads only as many items as the page returns. If the item a cursor names is deleted before the next request, that request returns the next page instead of 400. The page may repeat items, but no item is skipped. Cursors are opaque strings, and cursors returned before this change continue to work. A `cursor` value that the list did not return returns 400. - **Paused sandboxes started from a custom image survive the loss of their host.** A paused sandbox started from your own image is no longer stopped when its host is lost, as long as its saved copy is complete and another host holds the image. Its next wake restores it from that copy on a host that holds the image. While no such host can take it, a wake returns `503 no_capacity` with the detail that it starts from its saved copy as soon as one has room. If the image is deleted or rebuilt, or no other host holds it, the sandbox is stopped as before, and its parked charge ends when its host was lost. - **A wake whose host does not answer fails within about a minute.** A paused sandbox whose host did not answer its wake used to stay in `resuming` for as long as the host stayed silent, and every request on it waited. The wake now gives up after two unanswered attempts (about a minute): requests on the sandbox return `500 operation_failed` immediately, its memory and files are kept, and nothing is charged. A minute later the host is asked again; once it answers, the sandbox is `paused` and the next request wakes it. - **App versions and Render deploys.** `GET /v2/apps/{id}/versions` lists an app's versions, newest first and paginated by `limit` and `cursor`. `GET /v2/app-versions/{id}` returns one version. A version is numbered by the app generation that created it, so the response to the `PATCH` that creates a version identifies it. A new `spec.rollout` value creates a new version with the same settings. Render's deploy endpoints are now served under `/compat/render/v1`: list deploys (filterable by `status`), get a deploy, create a deploy (`imageUrl`, or a redeploy of the current image), and roll back to an earlier deploy (`POST /services/{serviceId}/rollback`). A deploy's ID is its version number. Creating a deploy, or changing the image with `PATCH /services/{serviceId}`, clears an earlier rollback, so the new version goes live. `commitId` and `buildId` return 400, and cancelling a deploy is not supported. ## 4 November 2026 - **Render API for Apps.** The web service endpoints of Render's REST API are served under `/compat/render/v1`, so Render API clients and the `render` CLI manage Apps by changing only their host (`RENDER_HOST`) and API key (`Authorization: Bearer `). Supported: create an image-backed web service (`POST /services`, `type` `web_service`, `runtime` `image`), list, get, update its name, image, plan and health check path, scale (`POST /services/{serviceId}/scale`) and delete. Lists return `[{"cursor", "service"}]` and are paginated with `cursor` and `limit` (20 by default, at most 100), and can be filtered by `name`. The plans `starter` (the default), `standard`, `pro`, `pro_plus`, `pro_max` and `pro_ultra` create an App with 1, 1, 2, 4, 4 or 8 vCPUs and 0.5, 2, 4, 8, 16 or 32 GiB of memory, billed by measured use; each copy serves on port 10000, Render's default `PORT`. Errors return Render's status codes and `{"id", "message"}` body. A field or operation Apps does not support yet, such as `repo` or deploys, returns 400 or 404. - **Desktops: E2B Desktop API.** Desktops now accept E2B's sandbox REST API with its `desktop` template at `/compat/e2b-desktop`: set `E2B_API_URL` to `https:///compat/e2b-desktop` and `E2B_API_KEY` to a Runtime API key, and E2B Desktop's SDKs (`@e2b/desktop`, `e2b_desktop`) can get, list (paginated by `nextToken`), connect to, extend the timeout of, pause and kill Runtime desktops. Creating a desktop from the `desktop` template with `timeout` and `metadata` is supported over the REST API; a timeout under 60 seconds returns 400. The SDKs' screen, mouse, keyboard, screenshot and stream methods, and their `Sandbox.create()`, which starts the screen through commands inside the sandbox, are not yet available. Desktops also gain `spec.endsAt` (a later time extends the timeout), and Browsers and Desktops gain `status.endsAt`. - **Launch checks on /status.json.** `GET https://withruntime.com/status.json` now returns `launch_checks`, one entry per product that launches with the next platform release (Browsers, Desktops, Notebooks, Dev environments, Actions runners, Cron, Apps, Functions, Containers, Load balancers, Firewall, Uptime checks, KV, Queues and Sites). Each hour, a test account creates, uses, reads back and deletes one resource of each product through the public API and confirms that its usage was billed. Three more entries run the same steps through a competitor's own SDK against Runtime's compatible API: `browsers_browserbase` (Browserbase's Node.js SDK), `kv_cloudflare` (Cloudflare's SDK) and `functions_lambda` (the AWS SDK for JavaScript's Lambda client). Each entry returns `state` (`passed`, `failed` or `not_served`), `checked_at`, `total_ms`, and `steps_ms` with the time of each step; a failed entry also returns `failed_step` and `error`. - **Upstash QStash schedules API for Cron.** QStash's schedules endpoints are served under `/compat/qstash`, so the `@upstash/qstash` client manages Cron jobs by changing only its base URL and token. Supported: create (`POST /v2/schedules/{destination}` with `Upstash-Cron`, `Upstash-Method` and the request body), get, list, pause, resume and delete. Each schedule is a job that sends the request to the destination on the cron schedule in UTC, with up to 3 retries, and is billed by measured use. A header not supported yet, such as `Upstash-Retries` or `Upstash-Forward-*`, returns 400 with its name. Errors return QStash's status and `{"error"}` body. - **Balancer targets receive requests only after their first health check passes.** When a balancer has `spec.health`, each target starts out of rotation: a new target, or every target on a host whose router has just restarted, receives no requests until its first health check passes, which happens within about 100 ms. One passing check is enough, whatever `healthyThreshold` is. While no target has passed yet, a request waits for the first check result instead of going to every target. If no target passes, requests go to all targets, as before. A target whose first check fails is reported unhealthy immediately. - **Start action for Dev environments.** `POST /v2/dev-environments/{id}:start` starts an environment immediately: a paused environment is woken, and a stopped one starts again from its disk, even when `spec.state` is already `running`. If `spec.state` holds another state, it is set to `running`. The response is the environment. Returns 409 (`environment_stopping`) while a stop is in progress, and 409 (`environment_not_resumable`) when the environment's disk was not kept. Every SDK has a `start` method. GitHub Codespaces' start (`POST /user/codespaces/{codespace_name}/start` under `/compat/github`) uses it, so it also starts a codespace that was shut down for being idle. - **Cloud Run: `gcloud run revisions list --service`.** In the Google Cloud Run Admin API at `/compat/google-cloud-run`, v1's `namespaces.revisions.list` now returns a service's revisions when `labelSelector` is `serving.knative.dev/service=`, as `gcloud run revisions list --service ` sends it. Each revision is a version of the Container, as a Knative Revision, and only revisions of the service in the request's project and location are returned. A request without that selector, such as `gcloud run revisions list` with no `--service`, or with any other selector, returns 400. The v1 `namespaces.revisions.list` and `namespaces.services.list` methods also accept `limit` and `continue`: with either, the response is one page, and the next page's token is in `metadata.continue`. Without them, the response holds every item. A repeated `labelSelector`, `limit` or `continue` returns 400. - **Containers: versions, and Cloud Run revisions.** `GET /v2/containers/{id}/versions` returns a container's versions, newest first and paginated with `limit` (1 to 100, default 20) and `cursor`. `GET /v2/container-versions/{id}` returns one version by its ID, which is the container's ID, a dot, and the version's number (`ctr_....3`). Both accept the container's slug in place of its ID and require the key's `containers:get` permission. Each version returns its `number`, `state` (`building`, `warming`, `ready`, `live`, `failed` or `superseded`), `image`, `imageDigest`, `failure`, the container `generation` that made it, and the container's `labels`. The SDKs read them with `containers.versions(id)` and `containers.version(id)`. In the Google Cloud Run Admin API at `/compat/google-cloud-run`, `services.revisions.list` (paginated with `pageSize` and `pageToken`) and `services.revisions.get` now return these versions as revisions named `-`, and v1's `namespaces.revisions.get` (`gcloud run revisions describe`) returns one as a Knative Revision. A revision of another service, location or project returns 404. Deleting a revision is not yet available, and v1's revisions list returns 404. - **Sandboxes: a fork waits for room, as a create does.** When no server has room for a copy, `fork` now retries with the same idempotency key for up to two minutes, as `create`, `wake` and `restart` do, in every SDK and in `withruntime sandboxes fork`. A fork refused for room pauses nothing and charges nothing. Set the wait as for a create (`waitForCapacityMs` in JavaScript, `wait_for_capacity` in Python and Ruby, `WaitForCapacity` in Go, `waitForCapacity` in Java); 0 fails immediately, and the CLI's `--no-wait` fails immediately. A fork of one copy now needs only the room a create of the same size needs; before, it needed four times its memory free on one server. A fork of many copies is still held to a quarter of each server's free memory past its first copy there, and is refused with `details.fits` when fewer copies fit. A fork that has already failed returns `retryable: false`, because its idempotency key returns the same error. - **Containers: Cloud Run revisions are named, and traffic can be pinned to one.** In the Google Cloud Run Admin API at `/compat/google-cloud-run`, each version of a Container is a revision named `-`, such as `hello-3`. In v2, a service now returns `latestCreatedRevision` (the newest version), `latestReadyRevision` (the version serving requests) and `trafficStatuses`. In v1, it returns `status.latestCreatedRevisionName`, `status.latestReadyRevisionName` and `status.traffic[].revisionName`. Sending 100% of traffic to one revision sets the Container's `spec.version`, which rolls it back to that version: in v2, `traffic` with `type: TRAFFIC_TARGET_ALLOCATION_TYPE_REVISION` and `revision`; in v1, `spec.traffic` with `revisionName`, as `gcloud run services update-traffic --to-revisions hello-2=100` sends. Traffic to the latest revision (`TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST`, or `latestRevision: true`) removes the pin. A revision of another service, a revision without a version number, or a revision named in a create returns 400. A traffic split between revisions returns 400, and `revisions.list`, `revisions.get` and `revisions.delete` are not yet available. - **Better Stack TCP monitors keep their type.** In the Better Stack compatibility API, a `PATCH` that changes a TCP monitor's `monitor_type` to `status`, `keyword` or `expected_status_code` now returns 400; previously it changed only the reported type while the check kept probing TCP. Delete the monitor and create a new one to change its type. - **Better Stack TCP monitors.** The Better Stack compatibility API at `/compat/betterstack` now accepts `monitor_type: "tcp"` with `url` (the host) and `port`, and creates an Uptime check on `tcp://:`; the monitor returns its host and port as Better Stack does. A TCP monitor without `port` returns 400. A TCP monitor's host and port, and a change to or from `tcp`, can't be updated, because an Uptime check's URL is fixed: such a request returns 400 (a `port` must be sent with its `url`); delete the monitor and create a new one. - **Better Stack expected-status-code monitors.** The Better Stack compatibility API at `/compat/betterstack` now accepts `monitor_type: "expected_status_code"` with `expected_status_codes` (a list of HTTP status codes); the Uptime check passes only those codes, and the monitor returns them as numbers. A request with that type and no codes returns 400. On other monitor types the codes are ignored, as on Better Stack, and changing a monitor to another type restores the default passing statuses (200 to 399). - **Browserbase sessions accept `keepAlive`.** `POST /compat/browserbase/v1/sessions` accepts `keepAlive: true` (previously 400). On Browsers, every session runs until its `timeout` (default 3600 seconds) or until you release it with `REQUEST_RELEASE`, whether or not a client is connected. A session created without `keepAlive` also keeps running after its client disconnects. To stop billing for it, release it or set `timeout`. - **Containers: a `slug` addresses a container, and Cloud Run services keep the name requested.** Containers accept an optional `spec.slug`: 1 to 63 lowercase letters, digits and hyphens, starting with a letter and not ending with a hyphen, unique within the project and set at create only. A create that repeats a slug in use returns 409 `conflict`, and the slug is released when the container is deleted. `GET`, `PATCH` and `DELETE /v2/containers/{id}` accept the slug in place of the ID; other kinds whose `name` is unique within the project, such as queues, accept that name the same way. A request by name requires the key's `get` permission on the kind. In the Google Cloud Run Admin API v2 at `/compat/google-cloud-run`, `services.create` now stores `serviceId` as the container's slug: the service's `name` is `projects/{project}/locations/{location}/services/{serviceId}`, and `services.get`, `services.patch` and `services.delete` accept that name. A second create with the same `serviceId` in the project returns 409 instead of returning the first service; a service created without `serviceId` is named by its container ID, as before. A slug is unique within a project, not within each location. A service's `description` now returns 400 instead of being stored as the container's `name`. - **Browserbase session list rejects an unknown status.** `GET /compat/browserbase/v1/sessions` returns 400 when `status` is not one of `RUNNING`, `ERROR`, `TIMED_OUT`, `COMPLETED` or `PENDING` (the values are case-sensitive), as Browserbase does. Previously it returned an empty list. - **Console: the Sandboxes, Browsers and Desktops lists show every sandbox, wherever it runs.** For many accounts the lists showed only sandboxes made before the account moved to Runtime's new servers, and none made since. The lists now include them for every account. A sandbox that moved while paused shows its current state; before, it showed paused, and a woken one was missing from the Running tab. "Stop all long-running" reads the same list. ## 3 November 2026 - **Better Stack's monitors API over Uptime checks.** Better Stack Uptime API calls for monitors (`GET` and `POST /api/v2/monitors`; `GET`, `PATCH` and `DELETE /api/v2/monitors/{id}`) run against Runtime Uptime checks at `/compat/betterstack` with a Runtime API key as the bearer token. Status and keyword monitors are mapped: URL, name, HTTP method, check frequency, request timeout, regions and required keyword. Any other field set in a request, including `paused: true` and `follow_redirects: true`, returns 400 with a message that names the field. Only a `keyword` monitor checks `required_keyword`, as on Better Stack; a keyword monitor requires it, and a monitor changed to `status` stops checking for it. A monitor's `url` can't be changed; delete the monitor and create a new one. - **GitHub Codespaces API for Dev environments.** The Codespaces endpoints of GitHub's REST API are served under `/compat/github`, so Octokit and other GitHub clients manage Dev environments by changing only their base URL and token (`Authorization: token ` or `Bearer `). Supported: create in a repository (`POST /repos/{owner}/{repo}/codespaces`), list, list in a repository, get, update the display name, start, stop, delete, and machine types for a repository or a codespace. Lists are paginated with `page` and `per_page` (30 by default, at most 100), return the full count in `total_count`, and return a `Link` header to the next and last pages. The machine types `basicLinux32gb`, `standardLinux32gb` (the default), `premiumLinux` and `largePremiumLinux` create an environment with 2, 4, 8 or 16 vCPUs and 8, 16, 32 or 64 GiB of memory, billed by measured use. Stop pauses the environment with its memory kept. A field Dev environments does not support yet, such as `devcontainer_path`, returns 400 with its name. - **Containers: Google Cloud Run Admin API v2.** Containers now accept the Cloud Run Admin API v2 services methods at `/compat/google-cloud-run`: set the client's endpoint to `https:///compat/google-cloud-run` and use a Runtime API key as the bearer token. `services.create`, `services.get`, `services.list` (paginated with `pageSize` and `pageToken`), `services.patch` and `services.delete` are supported; each service is a Container labeled with its project and location, and a service under another project or location returns 404. One container's `image`, `command`, `args`, `env` (name and value pairs), `workingDir`, one port, `resources.limits.cpu` (1, 2, 4 or 8) and `resources.limits.memory` (512Mi to 32Gi), `scaling.minInstanceCount`, `scaling.maxInstanceCount` and `maxInstanceRequestConcurrency` map to the Container's spec, with Cloud Run's defaults: a service scales to zero unless `minInstanceCount` is set. Any other field returns 400 with a message naming it. Create, patch and delete return their operation with `done: true`, so clients do not poll. `services.patch` replaces the whole template, so its body must include `template.containers[0].image`; `updateMask` and `etag` return 400. A service's name holds its Container ID, not the `serviceId` requested; `serviceId` follows Cloud Run's rule (1 to 49 lowercase letters, digits and hyphens, starting with a letter) and is the create's idempotency key for 24 hours, so a retried create returns the same service instead of creating a second one; once that service is deleted, a create with the same `serviceId` creates a new one. Revisions, traffic other than 100% to the latest revision, `validateOnly`, `allowMissing` and the IAM methods are not yet available. - **Containers: flyctl and fly-go.** flyctl and Fly's Go client (fly-go) now run against Runtime's Fly Machines API unchanged. These clients discard the path of `FLY_FLAPS_BASE_URL` and send `/v1/apps/...`, so the `/compat/fly` base URL from the earlier entry did not reach them. Requests whose `User-Agent` starts with `flyctl/` or `fly-go/`, or that include flyctl's `Fly-Client-Parent` header, are now served Fly's API at the root of the API host. Set `FLY_FLAPS_BASE_URL`, `FLY_API_BASE_URL` and `FLY_UIEX_BASE_URL` to `https://api.withruntime.com` and `FLY_API_TOKEN` to a Runtime API key, so that no flyctl request is sent to Fly. `fly machine run`, `list`, `stop`, `start` and `destroy` are supported. `GET /v1/apps/{app_name}`, the organization lookup and the image lookup that `fly machine run` performs are supported. `GET /v1/apps/{app_name}/machines/{machine_id}/wait` returns 200 when the machine is in a requested `state`, at the requested `version` or `instance_id` if one is given, and 408 immediately when it is not, because a Container's state changes only through a request. `from_event_id` is accepted and ignored. `GET /v1/apps/{app_name}` returns 200 for any app name. A stop request now accepts `id`, `timeout` and `signal: "SIGTERM"`; any other signal returns 400. A machine's `instance_id` is a string, as Fly's clients expect. Other HTTP clients continue to use `https://api.withruntime.com/compat/fly` as their base URL. - **Queues: SQS-compatible status codes for authentication and size errors.** The SQS-compatible endpoint now returns 403 for `AccessDenied` and for every `InvalidClientTokenId`, and 413 for `RequestEntityTooLarge`; these errors previously returned 400 (and `InvalidClientTokenId` returned 400 or 403 depending on where the request was refused). `AddPermission` and `RemovePermission` are not supported and return 400 `UnsupportedOperation`: access to a queue comes from the access key's grant. - **Functions: AWS Lambda `Invoke`.** The AWS Lambda API on Runtime Functions now supports `Invoke`, so `invoke` in AWS's SDKs runs a function after you change only the endpoint and the keys. The payload is sent as a `POST` to the function's address (`status.url`). With `InvocationType` `RequestResponse` (the default), the call returns 200 with the function's response body. When the function returns a 4xx or 5xx status, the response also includes `X-Amz-Function-Error: Unhandled`. A function whose response is larger than 6 MB, whose connection closes before it responds, or that runs longer than 15 minutes also returns 200 with `X-Amz-Function-Error: Unhandled`, and an `errorType` of `Function.ResponseSizeTooLarge`, `Runtime.ExitError` or `Sandbox.Timedout`, so SDKs do not retry an invocation that may have run. When the function cannot accept the request, `Invoke` returns 429 `TooManyRequestsException` or 500 `ServiceException`. `DryRun` returns 204 without running the function, including for a function that has no address yet. `Event` is not yet supported and returns 400 `InvalidParameterValueException`. A function that has no address yet returns 409 `ResourceConflictException`. A payload larger than 6 MB returns 413 `RequestTooLargeException`. Concurrent invocations are limited per organization; past the limit, `Invoke` returns 429 `TooManyRequestsException`. `Qualifier`, `LogType: Tail` and `ClientContext` are not supported. - **Browserbase session list filters on Browsers.** `GET /compat/browserbase/v1/sessions` (`sessions.list` in Browserbase's SDK) accepts `status` (`PENDING`, `RUNNING`, `COMPLETED` or `ERROR`) and `q` in Browserbase's metadata query form, `user_metadata['key']:'value'`, and returns only the sessions that match both. A session whose browser is still starting now reports `PENDING` instead of `RUNNING`. A `q` in any other form, including a nested path such as `user_metadata['a']['b']:'value'`, returns 400: session metadata on Browsers holds only string values at the top level. - **Browserbase's session debug URLs on Browsers.** `GET /compat/browserbase/v1/sessions/{id}/debug` (`sessions.debug` in Browserbase's SDK) returns the session's `wsUrl` (its Chrome DevTools Protocol WebSocket URL) and, when the browser draws its screen, its live view as `debuggerUrl` and `debuggerFullscreenUrl`; otherwise those are empty. `pages` is always empty; the browser's pages are listed at its `httpUrl` (`/json/list`). - **Queues: SQS-compatible error status codes.** The SQS-compatible endpoint now returns the HTTP status Amazon SQS returns for each error. For example, `DeleteMessage` with an invalid receipt handle and `CancelMessageMoveTask` for a task that does not exist now return 404 instead of 400. Every operation and error is checked against AWS's published SQS API model. - **Checkly's checks API over Uptime checks.** Checkly API calls for API and URL checks (`POST /v1/checks/api`, `POST /v1/checks/url`, `PUT` on each, `GET /v1/checks`, `GET` and `DELETE /v1/checks/{id}`) run against Runtime Uptime checks at `/compat/checkly` with a Runtime API key as the bearer token. Frequency, locations, maximum response time, the URL, the method and one status-code assertion are mapped; any other field set in a request returns 400 with a message that names the field. - **Balancers and Ports: target weights and failover.** Each entry in `spec.targets` accepts `weight` (1 to 1,000, default 1) and `backup` (default `false`). When targets have different weights, each request (or, for a port, each connection) goes to a target with a probability of its weight over the sum of the weights; when weights are equal, it goes to the target with fewer requests in flight of two chosen at random. On a balancer, a backup target receives requests only while no other target is healthy and has capacity; on a port, it receives connections only while every other target's sandbox is stopped or deleted. Available in every SDK. - **Apps, Functions and Containers: health checks while serving.** `healthPath` is now also checked while a copy serves: every 5 seconds, each copy is sent a GET of the path, and a copy that fails 2 consecutive checks (no status under 500 within 2 seconds) receives no requests until it passes 2 consecutive checks. Previously the path was checked only before a copy took its first request. During a staged release that changes `healthPath`, the service's address checks only that each copy's port accepts a connection until the release completes. A `healthPath` that does not start with `/` is checked as `/`, as the check before the first request already does. A path with characters a URI path cannot contain, such as a space or `#`, is not sent: the check only confirms that the copy's port accepts a connection. - **Containers: Fly Machines restart.** `POST /compat/fly/v1/apps/{app_name}/machines/{machine_id}/restart` is now supported. It creates a new version of the machine's Container with the same settings, and that version's copy replaces the running one. The `timeout` query parameter is accepted. `signal` returns 400, because the restart replaces the copy instead of signaling its process. As with the other machine routes, a machine is found only under its own app. - **Speed page: daily history charts and provider comparison.** The [speed](./speed) page now charts each listed operation's median own time for each of the last 90 days, against its physical floor. Hover over a day to see its p95, p99 and sample count. When another provider has been measured the same way and Runtime is faster, the page also lists it beside the matching Runtime operation, comparing total times. - **CI/CD: a job that fails because its machine ran out of disk says so in its reason.** When a job's machine runs out of disk, the error is often never written to the job's log, because the log is on the same full disk. Such a job's reason was the log's last error, for example `error: build failed, waiting for other jobs to finish...`. The job's log now ends with a line that says the machine ran out of disk (`error: No space left on device: ...`), and its reason names that line. - **Speed: browser create time and rival providers, measured the same way.** `browsers.create` is now measured every five minutes. The time runs from `POST /v1/browsers` until the browser's CDP WebSocket answers `Browser.getVersion`, which is the point where Playwright or Puppeteer can connect. The CDP exchange carries no `Server-Timing`, so all of its time is counted as Runtime's own. `speed.json` also includes a `rivals` array. Each rival is timed from the same location as Runtime, through the provider's own public API, to the same outcome as the Runtime operation it is compared with (`compares`). Browserbase, Steel, Hyperbrowser and Browser Use are timed until CDP answers. E2B, Daytona and Vercel Sandbox are timed until the sandbox is running. Rivals return no `Server-Timing`, so each line reports total time only. A rival is listed only after it has been measured with its own API key; nothing is estimated. Each rival's history is available from `speed/history.json?op=rivals..` ([speed](./speed)). - **Sites (beta): static websites served with no machine.** A site is files and a route table (redirects, rewrites, headers, Markdown for agents, 404 pages) served by every front. `POST /v2/sites` creates one; `:missing` returns the files, by SHA-256, the site does not hold yet, `:upload` stores each, `:release` stages a release and `:publish` makes it live in one commit. `:rollback` publishes the previous release, also in one commit, and keeps the release it replaced so it can be published again. Both `:publish` and `:rollback` accept `ifVersion` and return 409 if another change came first. `:releases` returns the live release, the earlier ones and the ones rolled back from. A deleted site stops serving and keeps its releases; restoring it serves its live release again. `npx withruntime sites deploy ./dist` runs all of these for a folder and uploads only the files that changed. Every SDK, the CLI and the MCP tools include Sites. Usage is billed on the existing storage, operations and egress meters at their current rates. - **Speed history by the hour and by the day.** Each listed operation's hourly and daily p50, p95 and p99 (Runtime's own time, the network's part and the total), sample count and failure count are now kept indefinitely and returned by `https://status.withruntime.com/speed/history.json?op=&step=hour|day&from=&to=`. `from` and `to` take a date (`2026-11-03`) or an ISO 8601 time and are rounded down to the step; by default a request returns the last 48 hours, or the last 365 days with `step=day`. A request returns at most 744 hourly or 3,660 daily points, and returns 400 for an unknown operation or an invalid range. Percentiles are computed from each hour's or day's own samples. Responses are cached for one minute ([speed](./speed)). - **Containers: Fly Machines API.** Containers now accept Fly's Machines REST API at `/compat/fly/v1`, so clients written for `api.machines.dev` run against Runtime after you change only the base URL and the token: use `https:///compat/fly` as the base URL and a Runtime API key as the bearer token. For flyctl's Machines calls, set `FLY_FLAPS_BASE_URL` to the same base URL. Each machine is a Container with one copy, labeled with its app. Creating, showing, updating and deleting a machine, listing an app's or an organization's machines, starting, stopping and suspending a machine, and reading and writing its metadata in all six forms are supported. A machine's `image`, `env`, `guest.cpus`, `guest.memory_mb`, `init.cmd`, `init.entrypoint`, `metadata`, one service's `internal_port`, one mount and `restart.policy` (`no` or `always`) map to the Container's spec; any other config field returns 400 with a message naming it. As on Fly, an update replaces the machine's whole config, so a field it omits is removed; listing machines returns every machine, not one page; and a machine ID is found only under its own app, so a request under another app returns 404. `auto_destroy: true` returns 400. Restart, wait, events, versions, exec, signal, processes, leases, cordon and the memory routes are not yet available and return 404. - **Sandboxes: E2B API.** Sandboxes now accept E2B's REST API at `/compat/e2b`, so code that uses E2B's SDKs (`e2b`, `@e2b/code-interpreter`) can create and manage Runtime sandboxes after you change only the API URL and the API key: set `E2B_API_URL` to `https:///compat/e2b` and `E2B_API_KEY` to a Runtime API key. Creating a sandbox from the `base` template with `timeout`, `metadata` and `autoPause`, getting and listing sandboxes (paginated by `nextToken`), setting the timeout, pausing, connecting (which resumes a paused sandbox) and killing are supported. Running commands and reading or writing files inside the sandbox through E2B's SDK are not yet available. A request that sets an unsupported field, such as `envVars` or another template, returns 400 with a message naming the field. - **Browserbase's sessions API on Browsers.** Browserbase's Node.js SDK (`@browserbasehq/sdk`) works with Runtime Browsers when `BROWSERBASE_BASE_URL` is set to `https:///compat/browserbase` and `BROWSERBASE_API_KEY` to a Runtime API key. `POST /v1/sessions` returns a running session with its `connectUrl` (the browser's Chrome DevTools Protocol WebSocket URL), `GET /v1/sessions/{id}` and `GET /v1/sessions` read and list sessions (a list of more than 100,000 sessions returns 400), and `POST /v1/sessions/{id}` with `status: "REQUEST_RELEASE"` ends one. Fields and operations not yet supported (`region`, `keepAlive`, proxies, contexts, debug URLs, recordings) return 400 or 404 instead of being ignored. - **Functions: AWS Lambda API.** Functions now accept the AWS Lambda API, signed with AWS Signature Version 4, so code that uses AWS's SDKs (such as boto3) runs against Runtime Functions after you change only the endpoint and the keys. Create an access key pair with `POST /v2/aws-access-keys`, authenticated with a Runtime API key; it returns `accessKeyId` and `secretAccessKey`, reaches what that API key reaches, and stops working when that API key is revoked or expires. Set `AWS_ENDPOINT_URL_LAMBDA` to `https://`, set `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` to the pair, and use a function's ID as `FunctionName`. `GetFunction` (`FunctionName`, `MemorySize`, `Environment` and `LastModified`), `ListFunctions` with `Marker` and `MaxItems`, and `DeleteFunction` are supported. Errors return Lambda's exception names, such as `ResourceNotFoundException`. A qualified function name or an ARN returns 400 `InvalidParameterValueException`. `Invoke`, `CreateFunction`, `UpdateFunctionCode`, `UpdateFunctionConfiguration`, versions and aliases are not yet available and return 404. - **Notebooks: connect JupyterLab, VS Code or any Jupyter client.** The URL that `POST /v2/notebooks/{id}:open` returns now also accepts its secret as a Jupyter token, sent as `Authorization: token ` or `?token=`. Jupyter clients use Jupyter Server's REST API (`/api/kernels`, `/api/sessions`, `/api/contents`) and the kernel WebSocket without changes, and share the kernel with cells run through `:execute`. - **Firewalls.** `POST /v2/firewalls` creates a named rule set that ports and balancers attach to with `firewallId`. Each rule blocks, allows or rate-limits HTTP requests by IP address or CIDR block, path, method or header; a rule that matches ports applies to raw TCP connections only. Rules are checked at write by the same parser and compiler every host enforces them with. A change to a firewall applies to every route attached to it one commit later, without a write to the routes. A rate limit counts each client across every route attached to the firewall. A route with `firewallId` takes no inline `firewall`; the request returns 400. A route attached to a deleted firewall refuses every visitor. Available in the API, the SDKs, the CLI and the MCP tools. - **KV: Cloudflare Workers KV API.** KV stores now accept Cloudflare's Workers KV REST API at `/compat/cloudflare/client/v4`, so code that uses Cloudflare's SDKs runs against Runtime KV after you change only the base URL and the API token: set `baseURL` to `https:///compat/cloudflare/client/v4`, `apiToken` to a Runtime API key, and use a KV store's ID as the namespace ID. Reading, writing and deleting a value, and listing keys with `prefix`, `limit` and `cursor`, are supported. A request that sets expiration or metadata returns 400 with a message naming the unsupported parameter; bulk operations and namespace management are not yet available and return 404. - **Browsers: capture a page with `POST /v2/browsers/{id}:screenshot`.** Opens `url` in a new tab, waits for its load event, and returns the image as base64 with the page's final URL, title and HTTP status; without `url`, it captures the page the browser shows now. Set `fullPage` to capture the whole page, `format` to `png` or `jpeg`, and `waitMs` (up to 10000) to wait for late scripts. The capture runs inside the browser's own sandbox. Returns `409` (`browser_not_running`) when the browser is not running and `504` (`page_timed_out`) when the page does not load within 30 seconds. Available in every SDK as `browsers.screenshot`. - **Balancers: health checks.** A balancer's `spec.health` sets how each target is checked, with the fields of an AWS ALB target group: `path`, `intervalSeconds`, `timeoutSeconds`, `healthyThreshold` and `unhealthyThreshold`. A target that fails `unhealthyThreshold` consecutive checks receives no requests until it passes `healthyThreshold` consecutive checks. Available in every SDK. - **Sandboxes: upload links replace files in root-owned folders.** A `PUT` link from `POST /v1/sandboxes/{id}/files:link` to a path under `/workspace` whose folder is owned by root (for example, a tarball extracted with `sudo`) returned `404` with `This upload is not open in the sandbox.` The upload now replaces the file atomically, as `files_write` does, and the new file is owned by the sandbox user. When the sandbox user cannot write the folder (outside `/workspace`), the link returns `403 permission_denied` with the folder named, instead of `404`. - **GitHub Actions runners in every SDK.** `/v2/runners` is now in the API reference and the OpenAPI document, and every SDK makes its calls as `runners.settings`, `runners.enable`, `runners.disable`, `runners.jobs` and `runners.job`. `GET /v2/runners/jobs` returns `{"jobs": [...]}`; `GET /v1/actions/jobs` still returns the array. - **Sandboxes: lists and lookups include every sandbox of an organization being moved to Runtime 2.0.** While an organization has sandboxes on both Runtime 1.0 and Runtime 2.0, `GET /v1/sandboxes` returns them all in one paginated list, in the same order and with the same fields as before. `GET`, exec, files and `DELETE` on a sandbox return from wherever it runs, and the console's Sandboxes page shows both. Before, a sandbox created on Runtime 2.0 might be missing from the list, and requests for it returned 404. - **Plans: `includedMicros`, `yearlyOnly` and `trialUsageMicros`.** `GET /v1/plans` returns three new fields on each plan: `includedMicros`, the usage included each month (null when the fee and `bonusMicros` are the plan's usage); `yearlyOnly`, whether the plan is billed yearly only (choosing such a plan monthly returns `409`); and `trialUsageMicros`, the usage a trial includes (`"0"` for none). Every current plan returns `null`, `false` and `"0"`. The TypeScript, Python, Go, Ruby and Java SDKs include the fields. ## 2 November 2026 - **Sandboxes: exec on a newly created sandbox returns as soon as the command exits.** In some regions, `POST /v1/sandboxes/{id}/exec` on a sandbox created at the default size could hang until the client's timeout. The first exec after a create now returns in about 35 ms, and later ones in about 6 ms. - **API keys limited to a compartment reach only that compartment's sandboxes.** On Runtime 2.0, a key limited to one compartment (project) now lists only that compartment's sandboxes, and `GET`, `DELETE`, `:exec`, files, processes and every other sandbox route return 404 for a sandbox in another compartment, the same response as for a sandbox that does not exist. A sandbox created on Runtime 2.0 before this change has no recorded compartment: a compartment's key reaches it only if that key created it. Account-wide keys still reach every sandbox in the account. A create with a `name` that a sandbox the key cannot reach holds returns 409 `name_taken` without naming that sandbox, and `getOrCreate` never returns it. - **CI/CD: a failed job's reason names its first error.** The reason shown for a failed job, in the console and on its GitHub check run, was the last line of its log, which for a build is often a line such as `error: build failed, waiting for other jobs to finish...`. It is now the first error line that names a cause (for example `error[E0308]: mismatched types`), or a `No space left on device` line when the machine ran out of disk. When a long log is shortened, every error line is kept, including `error:` lines without a code, and a compiler's quoted source lines are no longer taken for an error. - **API v2: filter sandboxes and other machines by `phase`.** The lists of sandboxes, browsers, desktops, dev environments, databases, Kubernetes clusters and notebooks (`GET /v2/{resources}`) accept `phase` (`pending`, `running`, `paused`, `stopped` or `failed`) and return only the resources in that phase. For example, `GET /v2/sandboxes?phase=running` returns the running sandboxes. The filter reads an index that is updated in the same write as the status, so a resource appears under its new phase as soon as its status shows it. The TypeScript, Python, Go, Ruby and Java SDKs take `phase` in each of these lists' filtered queries. - **API v2: lists return the API v1 ID of each moved resource with `include=movedFrom`.** Every v2 list (`GET /v2/{resources}`) accepts `include=movedFrom`, which adds `movedFrom` to the page: the ID of each item that moved in from API v1 mapped to the ID it had there. Items created in v2 are not in it. On a metered resource, `include=usage,movedFrom` returns both. Any other `include` value returns `400`. The TypeScript, Python, Go, Ruby and Java SDKs return the page's `movedFrom`, and the CLI takes `--include movedFrom`. - **API v2: lists return each resource's usage to date with `include=usage`.** A list of a metered resource (`GET /v2/{resources}`, for every resource that has `:usage`) accepts `include=usage`, which adds `usage` to the page: each item's ID mapped to what it has used from the month it was created (`since`) to now, with the same figures as `:usage` (`windows`, `heldMs`, `cpuBilledUsec`, `memoryMibMs` and `chargedMicros`), summed over those months. The page is still one request, so a list that shows each resource's cost no longer needs one `:usage` call per item. Any other `include` value returns `400`, as does `include` on a resource that is not metered. The TypeScript, Python, Go, Ruby and Java SDKs take `include` on `list` and return the page's `usage`, and the CLI takes `--include usage`. - **API v2: lists return resources newest first with `order=newest`.** Every v2 list (`GET /v2/{resources}`) accepts `order=newest`, which returns resources by `createdAt`, newest first, and pages with `cursor` as before. `order=oldest`, the default, keeps today's ID order. Labels and filters apply on top of either order. A key limited to the resources it created returns `400` for `order=newest`. The TypeScript, Python, Go, Ruby and Java SDKs take `order` on every `list`, and the CLI takes `--order oldest|newest`. - **Sandboxes: commands run as the sandbox user, and the HTTP proxy works at every size.** On some sandboxes, commands, processes, terminals and file writes ran as root even though the sandbox reported `user: "sandbox"`. They now run as the sandbox user (uid 1000) with its `HOME`, and files written through the files API are owned by that user, as on every other sandbox; `user: "root"` still runs a command through `sudo`. On those sandboxes, every request sent through the built-in proxy (`HTTPS_PROXY`, `http://127.0.0.1:10800`) at a size other than the default returned `502` with `Connection reset by peer`; the proxy now carries them. `/workspace` is owned by the sandbox user, the host name `runtime-guest` resolves (so `sudo` no longer prints `unable to resolve host`), and `PATH`, `NO_PROXY` and `NODE_USE_ENV_PROXY` are set as on every other sandbox. - **Sandboxes: bucket mounts succeed and stay mounted.** `POST /v1/sandboxes/{id}/mounts` checked whether the mount's client was still running by the size of its cgroup's process list, which the kernel always reports as 0. Every mount was therefore taken for a client that had exited: the request returned `422 mount_failed` with "the bucket did not mount", and the client was stopped even when the bucket had mounted. The check now reads the process list itself. A mount returns `200` once the bucket is mounted, `GET /v1/sandboxes/{id}/mounts` reports `mounted: true` for it, and `POST /v1/sandboxes/{id}/mounts:unmount` waits for the client to exit before it returns. - **Sandboxes: sessions work for every sandbox.** `POST /v1/sandboxes/{id}/sessions`, `GET /v1/sandboxes/{id}/sessions` and `POST /v1/sandboxes/{id}/sessions/{sessionId}:revoke` now work for every sandbox, and the `rtsess_` token a session returns can be used with every sandbox. Previously, these requests returned `409 served_elsewhere` for some newly created sandboxes. Sessions behave as before: a token reaches only its own sandbox's commands, files and previews, from the origins it lists, and is rejected with `401` on its next request once it is revoked or expires, or once the API key that created it is revoked. ## 1 November 2026 - **Sandboxes: browser, desktop and MCP on every new sandbox.** `/v1/sandboxes/{id}/browser` (`:start`, `GET`, `:stop`), `/desktop:start`, `/desktop:act`, `/desktop/screenshot`, `/desktop:stop` and `/mcp` (`POST`, `GET`, `DELETE`) previously returned 409 `served_elsewhere` for sandboxes created on the new platform. They now return the same request and response shapes as for any other sandbox. - **Sandboxes: network rules work for every sandbox.** `GET /v1/sandboxes/{id}/network` and `PUT /v1/sandboxes/{id}/network` now work for every sandbox. Previously, these requests returned `409 served_elsewhere` for some newly created sandboxes. The rules have not changed: a change applies to open connections too, so a connection the new rules refuse is closed, and the response reports `enforced: true` once the sandbox's server applies the new version. A sandbox created with `network` rules can now also be created from a snapshot; previously that request returned `501 feature_unavailable` for some newly created sandboxes. - **Sandboxes: a verb the API key does not allow returns 403, and older keys can wake, extend, update, set retention and restart.** A request for a sandbox operation that the API key's grant does not include now returns `403 forbidden`; previously some of these requests returned `409 served_elsewhere`, and retrying them at the address in the error returned the same error again. API keys created before November 1, 2026 that can create sandboxes can now also call `:wake`, `:extend`, `:update`, `:retention`, `:restart`, `:fork`, `:resize` and `:switch-image` on them, as newer keys can. No change to existing keys or code is needed. - **Sandboxes: a fork returns 409 `fork_unsafe` when a copy would repeat the source's random numbers.** A fork (`forkOf`, or `POST /v1/sandboxes/{id}:fork`) copies every process's memory, including the state of each language runtime's own random number generator. The kernel's generator is reseeded in every copy, but Node's UUID cache and OpenSSL generator, Python's `random` and `ssl` generators and Java's `SecureRandom` are reseeded only when the process loads Runtime's reseed shim. A fork whose source runs a Node, Python or Java process that the shim does not reach now returns `409 fork_unsafe`, names each such process, and makes no copy; previously the copies started and could return the same UUIDs, tokens and keys as the source and as each other. While a copy is made, the source's Node, Python and Java processes are paused for a few milliseconds, so no copy runs one of them before it is reseeded. - **Sandboxes: a fork with `count` above 1 makes its copies in parallel.** `POST /v1/sandboxes/{id}:fork` with `{"count": 4}` previously made each copy after the one before it; it now makes them at the same time and records them in one commit, so a fork of 4 copies of a running sandbox returns in 216 ms at the median instead of 340 ms. - **Tailscale: new sandboxes can join a tailnet again.** `POST /v1/sandboxes/{id}/tailscale`, `GET /v1/sandboxes/{id}/tailscale`, `DELETE /v1/sandboxes/{id}/tailscale`, and the `tailscale` field on `POST /v1/sandboxes` and `POST /v1/sandboxes/{id}:update` now work for every sandbox. Previously, these requests returned `501 feature_unavailable` for some newly created sandboxes. The rules have not changed: the auth key is read from your job secret with your key's own permission to reveal it and is never returned or logged. While a sandbox is on a tailnet, forking it or taking a memory snapshot of it returns `409 tailscale_joined`. If a create with `tailscale` cannot join, the sandbox is stopped and the request returns `422 tailscale_failed`. - **API: keep-alive connections stay open across Runtime releases.** When a server's API process was replaced during a release, a request in progress on a keep-alive connection could be answered with `connection: close`, and the client had to open a new connection. The connection now moves to the new process between two requests, after the last response is sent in full, and stays open. Connections whose last request had a body, HTTP/1.0 connections, and connections whose response included `connection: close` are closed after their response instead, as before. - **Jobs: a vouch is no longer cleared by secrets scoped to other sandboxes by ID.** Any change to an egress secret's scope, including a list of other sandboxes' IDs, cleared every vouch in the organization, and the job's runs carried no `job_vouched_by` claim in their identity tokens. Scopes by sandbox ID no longer affect a vouch, except for a run whose own sandbox ID is added to one, which carries no `job_vouched_by` claim. A change to a secret with no scope, a label or App scope, a secret's policy, or the upstream proxy still clears the vouch until an owner or admin vouches again. Vouches that were in effect when this change was released remain in effect. - **Keys: `POST /v1/keys` no longer slows down or returns 502 `keys_unavailable` in accounts with many keys.** In an account holding tens of thousands of keys, creating a key could take 15 seconds or more and return `502 keys_unavailable`, and other requests in the account waited while it ran. Creating a key now reads only the chain of keys that made the calling key and the keys under a maker with a daily limit, and completes in under 100 ms in an account with 50,000 keys. - **Sandboxes use included free usage before credit in every region.** A sandbox's running time is charged to the month's included free usage first, then to credit, in every region where the account runs sandboxes. A sandbox pauses only when both are used up; a wake then returns `402 insufficient_credit` until credit is added. At the start of each month, the new month's included usage applies immediately for accounts whose included usage renews monthly. - **API keys: keys Runtime creates for your builds, CI/CD checks, app versions, agent sessions and Actions runners no longer count toward the limit of 1,000 live keys per member.** Previously, a busy CI/CD repository could fill the limit with its checks' short-lived keys, and `POST /v1/keys` returned `409 limit_reached` to the member who connected it. Those keys are still bounded by the work that uses them and expire with it. - **`spreadFrom` keeps a new sandbox off its peers' servers.** `POST /v1/sandboxes` accepts `spreadFrom` again: 1 to 16 IDs of your own sandboxes. The new sandbox starts on a server that none of them is running on, and stays apart from them each time it is placed again. When no other server has room, the create returns `503 capacity_unavailable` immediately and nothing is made or charged. An ID this key cannot read returns `400 invalid_request`. Snapshot IDs are not accepted in `spreadFrom`, and a create that copies a snapshot returns `501 feature_unavailable` when it sets `spreadFrom`. - **Sandbox creates in a full region fail immediately and name the size.** When no server in the region has room for a new sandbox, `POST /v1/sandboxes` now returns `503 capacity_unavailable` immediately, before any state is written, and the error message states the vCPUs and memory that could not be placed. Retry after the `Retry-After` delay with the same `Idempotency-Key`, or request a smaller sandbox. - **Sandboxes: creates from an existing image no longer return 404 `image_not_found` for some accounts.** `POST /v1/sandboxes` could return `404 image_not_found` (or `snapshot_not_found`, `volume_not_found`) for an image, snapshot or volume that exists in the account. The create now succeeds in the same request, and a retry with the same `Idempotency-Key` returns the same sandbox. - **Sandboxes: creates no longer return 501 or an immediate 503 for some accounts.** `POST /v1/sandboxes` could return `501` (for example `env_unavailable` or `feature_unavailable`) or an immediate `503` for request bodies the API supports. A create rejected before anything was applied now succeeds in the same request, and a retry with the same `Idempotency-Key` returns the same sandbox. A `503 busy` whose create may have been applied is still returned, so a retry never creates a second sandbox. - **Correct clock after a restore.** A sandbox created from a template, forked, or woken from a pause now reads the correct wall-clock time from its first instruction. Previously, such a sandbox could read the time at which its memory snapshot was saved. - **Creates from an image that cannot boot return 404.** `POST /v1/sandboxes` now returns 404 `image_not_found`, naming the image and the reason, when the image is not the organization's or was deleted. Previously it returned 503 `busy`, so retries failed the same way. Nothing is created. - **Requests that carry a secret's placeholder are never sent unreplaced.** If the server running a sandbox has not received secret updates in the last 15 seconds, a request that carries a secret's placeholder, or that a secret's header rules apply to, returns `503` with `Retry-After: 1` and `X-Runtime-Egress: secret-not-current` instead of being sent with the placeholder. Retry the request. - **Files: watches work in sandboxes created from the default image.** `POST /v1/sandboxes/{id}/files/watches` (and `files.watch()` in the SDKs) returned `500 watch_failed` ("The watch exited while starting") in sandboxes created from the default image. A watch now runs in every image, and when one fails to start, the error message includes the exit code and the reason the sandbox reported. - **Sandbox creates move off a failed host immediately.** When the host chosen for a new sandbox has failed and refuses connections, `POST /v1/sandboxes` now places the sandbox on another host immediately, instead of returning `503` until the failed host's lease expires (about 2 seconds). If that host had already started creating the sandbox, the request still waits for it, so each `Idempotency-Key` creates at most one sandbox. - **A Function with no running copy starts one for its next request.** A request to a Function that has scaled to zero waits at the router while a copy is started from the version's saved copy, then is answered; it no longer returns `503 machine_no_capacity` after 10 seconds. `sleepAfterMs` now applies: a copy that requests no longer need keeps running for `sleepAfterMs` after its last request before it stops, so requests a moment apart reach a running copy. - **A crashed process restarts immediately.** When the process of an App, a Function or a container exits, its first restart now happens immediately instead of after 1 second; an App's process killed with `SIGKILL` answers requests again about 60 ms later. A process that exits again still waits 1 second, then 2, 4 and so on up to 60 seconds, and ten exits within 10 minutes still mark it `failed`. - **CI/CD: always run a pull request's own tests.** A split step with `own: true` (`split: { files: "**/*.test.{ts,tsx}", own: true }`) runs the test files a change adds or changes that no other step's `split` names. With such a step in `runtime.yml`, a pull request that adds or changes a test file no step runs (any language: TypeScript and JavaScript, Python, Go, or a Rust crate's files) fails with a `tests not run` job that lists each file and the reason. Configs without an `own` step are unchanged. - **Parallel forks no longer return `503 busy` while a snapshot copy arrives.** When a snapshot's own server was full, creating many sandboxes from that snapshot at once could return `503 busy` ("Cloud is busy; retry"). It happened when a create joined a copy of the snapshot that was arriving on another server. These creates now succeed, and the copy finishes arriving alongside them. - **CI/CD checks every push of the branches you list.** Add `pushes: [{ branch: next, steps: [...] }]` to the production branch's `.runtime/checks.yml` to check each push of a branch that has no pull request of its own, such as a staging branch. Each such check runs only the named steps, appears on GitHub as `runtime @ ` rather than `runtime`, and once started it runs to its own verdict instead of being replaced by a newer push. - **Slack triggers in the v2 Agents API.** `POST /v2/agents/slack/installs` returns a link that adds Runtime's Slack app to a workspace, valid for 15 minutes; `POST /v2/agents/slack/installs:finish` completes it with Slack's `code` and the link's `state`, and accepts only a key of the project that created the link. `GET /v2/agents/slack/teams` lists the project's workspaces and `DELETE /v2/agents/slack/teams/{team}` removes one. `POST /v2/agents/{id}/triggers` with `kind: "slack"` and `config: {team, channel}` adds a trigger: each new message in the channel starts a session, each reply in its thread is sent to the same session, and the session's replies, status, questions and result are posted in that thread. Triggers are read with `GET /v2/agents/{id}/triggers` and `GET /v2/triggers/{id}`, changed with `PATCH /v2/triggers/{id}`, paused and resumed with `POST /v2/triggers/{id}:pause` and `:resume`, and deleted with `DELETE /v2/triggers/{id}`. ## 31 October 2026 - **Sandbox creates no longer wait on an unresponsive host.** When the host chosen for a new sandbox stops responding before it starts creating the sandbox, `POST /v1/sandboxes` places the sandbox on another host, usually within 2 seconds, instead of waiting for that host for up to several minutes. If that host had already started creating the sandbox, the request waits for it, so that each `Idempotency-Key` still creates at most one sandbox. If the cell cannot confirm where the sandbox may be created, the request returns `503`; retry with the same `Idempotency-Key`. - **Stopping a sandbox always completes.** A stop that could not be recorded used to leave the sandbox in `stopping` permanently, and every request on it returned `500 operation_failed`. The stop is now retried automatically every 5 minutes until the sandbox is `stopped`; meanwhile requests on it still return `500 operation_failed` immediately, its files are kept, and nothing is charged. Sandboxes already stuck in `stopping` are stopped by the same retry. - **Previews wait for a server that is still starting.** Within 60 seconds after a sandbox is created or wakes, a preview or TCP port request to a port that is not listening yet now waits up to 10 seconds for the port to open, and is forwarded as soon as it does. Previously it returned `502` with `x-runtime-preview-error: port_closed` immediately, so a health check sent right after create failed until the server started. After 10 seconds the request still returns `502 port_closed`. Requests to a port that is already listening are not delayed. - **Forks of a sandbox in use no longer fail with `sandbox_not_paused`.** If a command or other request woke the source between the fork's pause and the start of its copy, `POST /v1/sandboxes/{id}:fork` returned `409 sandbox_not_paused`. Requests that wake a sandbox automatically now wait until the copy has started, up to one minute, and then proceed as before. - **Postgres read replicas follow the standby.** On a Postgres database with `spec.standby` set to `true`, each read replica now streams its changes from the standby instead of the primary, so no replica is ever ahead of the standby. When the standby is promoted after a host loss, the replicas keep running at the same host names and continue to receive changes from the new primary without being recreated. A database whose host is lost while its `state` is `paused` or `stopped` now also fails over to its standby, then pauses or stops, and its read replicas are removed; previously its standby and read replicas kept running, and were billed, until the database was deleted. - **Exec returns all of its output.** `POST /v1/sandboxes/{id}:exec` now returns every byte a command writes to stdout and stderr, up to 256 MiB of each stream in a JSON response and with no limit in a streamed one. Previously the JSON response kept at most 64 KiB of each stream for a command of 60 seconds or less, and 1 MiB for a longer one. The response adds `stdoutBytes` and `stderrBytes`, the bytes each stream wrote, and `truncated`. The new `maxOutputBytes` parameter limits the response to that many bytes of each stream; a stream it cuts has its truncation flag set to `true`. The TypeScript, Python, Go, Ruby and Java SDKs return the same output and take the same limit (`maxOutputBytes`, `max_output_bytes`, `MaxOutputBytes`). `withruntime sandboxes exec` takes `--max-output-bytes`. `runtime_sandboxes_exec` over MCP returns `stdoutBytes`, `stderrBytes` and `truncated`. Sandboxes started before this change keep the 64 KiB limit until they are stopped and started. - **Forks succeed when the source is woken during the fork.** `POST /v1/sandboxes/{id}:fork` pauses the source while it copies it. If another request woke the source during that time (for example, a command run on it), the fork returned `409 not_paused`, said the source stayed paused, and left its copies running. The fork now returns its copies, and the source keeps running. - **Apps and Functions accept a start command.** `spec.command` on `/v2/apps` and `/v2/functions` sets the process each copy runs, as an argument list, with `PORT` set to `spec.port`. When the process exits, it is restarted automatically, and its last 8 KiB of output is kept in the copy at `~/.runtime/process-.status`. A version that fails its health check includes that output in `status.failure`. When `command` is omitted, the image's start command runs, as before. - **A failed fork deletes the copies it started.** When `POST /v1/sandboxes/{id}:fork` fails after some copies have started, the copies that started are now deleted and no longer appear in `GET /v1/sandboxes`. The error's `details.startedSandboxIds` still names them, and `details.copiesDeleted` is `true`. When the error is a 5xx, a failure on Runtime's side (for example, no capacity for a later copy, a copy that does not reach `running` in time, or the source cannot be woken), none of their running time is billed and `details.copiesBilled` is `false`. When it is a 4xx, caused by the request or the account (a limit or quota, or a copy or the source deleted or changed during the fork), the time they ran is billed. A copy whose create completed after the fork had already failed is deleted too. A copy that could not be deleted at once is named outside `details.deletedSandboxIds`, `details.copiesDeleted` is `false`, and it is deleted within 15 minutes. If the server handling the fork stops before it returns, the fork is marked failed with a 500 once it has run longer than any fork can (15 minutes, plus about 20 seconds for each GiB of the source's memory), and its copies are deleted unbilled within 15 minutes after that. A fork that does not answer within the request deadline is a failure on Runtime's side, and its copies are not billed; if your client closes the connection before the fork answers, the copies are deleted and billed for the time they ran. Previously these copies kept running, and were billed, until you stopped them. - **Reads are served while a cell's storage is full.** When a cell's drives are short of space, `GET` requests now succeed instead of returning `503 api_unavailable`, including a project's first request after a front restarts. Requests that change state return `503 capacity_unavailable` with a `Retry-After` header; retry them with the same `Idempotency-Key`. - **Fixed: file shares and streams for sandboxes created with `POST /v2/sandboxes`.** In regions served by the new API, a sandbox created with `spec.fileShares` could fail with "mounts file shares, which this cell's machines do not", and `GET /v2/sandboxes/{id}/streams` returned 404 for any sandbox created through `/v2`. Volumes and file shares are now mounted as they are for `POST /v1/sandboxes`, and exec and pty streams reach the sandbox. One file share can be mounted read-write in several sandboxes on different hosts at once. - **Postgres synchronous standby with automatic failover.** Set `spec.standby` to `true` on a Postgres database to keep a synchronous standby on another rack. A commit is acknowledged only once it is on the standby too. If the database's host is lost, the standby is promoted in its place without a request: the database keeps its ID, host name, port and password, holds every acknowledged commit, and accepts writes again within seconds; a new standby is then made on another rack, and until it runs, commits are acknowledged without a copy on another rack. `status.standby.phase` is `running` once every commit waits for the standby. The standby is billed as a database of the same size that never pauses. - **Password sign-in is faster.** `POST /v2/auth/sessions` with an email and password now checks the password while the sign-in attempt is recorded, instead of after it, and reads the clock alongside the account lookup. On cell 1 the median sign-in dropped from about 47 ms to about 35 ms. Responses, throttling, and the attempt count are unchanged: an attempt is always recorded before the response is sent, and a throttled email is refused before its password is checked. - **`getOrCreate` always returns the one sandbox that holds a name.** Concurrent `POST /v1/sandboxes` requests with `getOrCreate: true` and the same `name` now return the same sandbox, including when that sandbox is paused, stopped with its disk kept, or being moved between regions. Previously, under load, a second sandbox could be created under the same name, and later calls by that name could return either one. A create with a `name` that cannot be placed at that moment, for example while capacity is short, returns 503 `busy` with `Retry-After` and creates nothing; retry it. In the Vercel-compatible TypeScript SDK, `Sandbox.getOrCreate` now creates a missing sandbox with `getOrCreate`, and so does `get_or_create_sandbox` in the Vercel-compatible Python SDK. Concurrent calls for the same name return the same sandbox instead of failing with 409 `name_taken`. `resume: false` still leaves a found sandbox paused. - **Sandbox list pagination returns every page.** `GET /v1/sandboxes` no longer returns `400 invalid_request` (`cursor`) on the second or later page for organizations whose sandboxes run in more than one cell. Each sandbox is returned exactly once, oldest first, and `nextCursor` stays an opaque string to pass back unchanged. `withruntime sandboxes ls` and every SDK's list iterator page through the full list. - **A terminal that cannot open names the missing file.** When a sandbox's image cannot open a terminal for a `pty` process or stream (for example, an image without a devpts at `/dev/pts`), the error now names the file that failed, such as `/dev/ptmx: No such file or directory`, instead of the system error alone. - **`idempotency_key_reused` errors include `details` on every route.** A 422 `idempotency_key_reused` now always returns `details.firstUsedAt` and `details.operation`, naming when the key was first used and for which operation. Some routes previously omitted `details`. - **A fork retried with its `Idempotency-Key` after a 503 now creates its copies.** When `POST /v1/sandboxes/{id}:fork` returns 503 because its copies could not be opened, nothing is created, and sending the same request again with the same `Idempotency-Key` now forks the sandbox, instead of returning `500 operation_failed` ("its copies were withdrawn") on every retry. A fork that succeeded returns its original response to a retry with its key, including after its copies are deleted, and never forks again. - **Pull request checks into non-default branches use at most half of an account's CI/CD machines.** Checks of pull requests whose base is a branch other than the repository's production branch now hold at most half of the machines the account can run at once, combined. The other half stays available for checks of the production branch and of pull requests into it, so those start immediately even when many pull requests into another branch are being checked. A check waiting on this limit shows the reason in its jobs: how many machines checks into other branches hold and the most they may hold. - **Requests that a secret's rules do not allow now return 403 from Runtime.** A request that carries an egress secret's placeholder in a credential header, to a method and path that none of the secret's `rules` allows, is no longer sent to the destination with the placeholder. It returns `403 Forbidden` with `X-Runtime-Egress: secret-rule-not-matched`, and the body names the secret, the method and the path. Previously the destination received the placeholder and typically returned `401` with no explanation. The `sandbox.secret_not_applied` event with reason `rule_not_matched` is still sent. - **A process start whose program is missing returns `422 program_not_found`.** When the program to run, or the interpreter it names (a script's `#!` line or a binary's loader), does not exist in the sandbox (for example, an image without `/usr/bin/env`, through which a command named without a path runs), `POST /v1/sandboxes/{id}/processes` now returns 422 with the code `program_not_found` and the missing path in `details.path`, instead of `502 guest_failed`. A `cwd` that does not exist returns `422 cwd_not_found`. Nothing runs, and a retry returns the same response. - **Interpreter `503 busy` errors include their cause.** When a code interpreter request returns `503` with code `busy`, `error.details.cause` now describes why the request could not be confirmed, for example that the interpreter process exited before it answered. Use it to tell a transient condition that a retry resolves from one that persists. - **Commands start faster in new sandboxes.** `POST /v1/sandboxes/{id}:exec` and process starts no longer wait for the API to look up the organization's egress secret placeholders. The sandbox supplies them from its host before every command, so each command still receives them. A command in a sandbox seconds old previously waited about 1.4 s for that lookup. - **Egress secrets reach every new sandbox.** A sandbox whose start carried no organization in its network rules trusted the egress CA but received none of its organization's secrets: their variables were missing from commands, and requests to the secrets' hosts were sent without credentials. Such a sandbox now gets its secrets like any other. - **SDK methods for workflow runs, steps and events.** The TypeScript, Python, Go, Java and Ruby SDKs add `workflows.start` and `workflows.runs` for a workflow's runs, `jobs.start` for a job's run, `jobRuns.getStep` and `jobRuns.recordStep` for a run's durable steps, and `jobEvents.send` for events. In TypeScript, `withruntime/workflows` exports them, and `runSteps()` authenticates with the run's `RUNTIME_RUN_TOKEN`. `PUT /v2/job-runs/{id}/steps/{name}` with none of `output`, `error`, `sleepUntil` and `event` now records the step with a `null` result instead of returning `400`, so a step that returns nothing is kept and does not run again. - **Run tokens for job and workflow runs.** Each run gets a token in its `RUNTIME_RUN_TOKEN` environment variable. Send it as `Authorization: Bearer` to `GET` and `PUT /v2/job-runs/{id}/steps/{name}` for the run's own steps, so a workflow no longer needs an API key in its `env`. A run token works only for its own run's steps. - **Idempotency keys for job and workflow runs expire after 24 hours.** An `Idempotency-Key` sent to `POST /v2/workflows/{id}/runs` or `POST /v2/jobs/{id}/runs` returns the run it started for 24 hours; after that, the same key starts a new run. - **Durable steps and events for workflows in the v2 API.** `POST /v2/workflows/{id}/runs` starts a workflow run with an `input` (and `POST /v2/jobs/{id}/runs` a job's run), once per `Idempotency-Key`. A run's code records each step's result with `PUT /v2/job-runs/{id}/steps/{name}` and reads it back with `GET`, so a continued run replays it; a step can sleep until a time or wait for an event, sent with `POST /v2/job-events`, and its run continues when the time or event arrives. - **Sandboxes: mount file shares.** `POST /v2/sandboxes` accepts `spec.fileShares`, a list of up to 16 `{name, path, readOnly}` entries; each file share of the project is mounted at its `path` before the sandbox's phase is `running`, and again each time it resumes or starts. The API key needs `file-shares:read` on each share, and `file-shares:write` on each one not mounted `readOnly`: a share the key cannot read, or that does not exist, returns 404; one mounted read-write by a key that may only read it returns 403. `fileShares` is set when the sandbox is created and cannot be changed. A sandbox with file shares cannot be forked, and a sandbox started from a snapshot mounts none. Writes to a share mounted `readOnly` return `EROFS`. Each `path` must be absolute, outside `/proc`, `/sys` and `/dev`, and neither at nor inside another share's path; otherwise the request returns 400. - **Alerts: count rules.** `PUT /v2/alerts/rules/{id}` now accepts rules with `"query": {"kind": "count", "selector": "...", "window_ms": ...}`. A count rule counts the log lines that match a LogQL stream selector and line filters (for example `{service_name="api"} |= "error"`) within the window, and fires when the count crosses its condition. Each line is counted once, as soon as it is accepted, so a count rule's webhook is sent about 10 ms after the line that fires it. - **Sandboxes: call the API without a key.** A sandbox created with `spec.permissions` (for example `["sandboxes:list", "sandboxes:create"]`) can call `https://api.withruntime.com` without an API key. Its host adds a short-lived token for that sandbox to each request and replaces any `Authorization` header the sandbox sends, so no credential is ever stored in the sandbox. Each action must be one the creating key allows; otherwise the create returns 403. A fork gets its own identity with its source's permissions. `permissions` cannot be changed after a sandbox is created. - **Alerts: delete a rule.** `DELETE /v2/alerts/rules/{id}` deletes a rule and returns `{"id": ..., "deleted": true}`, or 404 when no such rule exists. A rule can be deleted while its alerts are firing: evaluation stops within a second, and each firing alert is sent to the rule's webhooks once more with status `resolved`. The limit of 100 rules per organization now also holds when rules are created concurrently; a create past the limit returns 400. - **Logs: faster live tail.** `GET /v2/logs/v1/logs/tail` now delivers a line about 15 ms after it is accepted, down from about 100 ms. A tail now waits for new lines instead of checking for them on an interval. - **CI/CD setup can be a list of layers.** `setup:` in `runtime.yml` now also accepts a list of up to 16 commands, run in order. A push to the production branch keeps each finished layer as a snapshot, keyed to that layer, the layers before it, the machine size and the `cache` files. A later setup starts from the deepest layer that is ready and runs only the layers after it, so a change to the last layer does not run the earlier ones again, and a setup that is stopped or fails partway keeps the layers it finished. Pull request checks start from these snapshots but never write them. A `setup:` of one command works as before. - **Logs: live tail.** `GET /v2/logs/v1/logs/tail?query=` now streams matching log lines as Server-Sent Events, resumable with `Last-Event-ID`. A tail stays open for as long as the API key that opened it is valid. It ends with an `end` event with code `tail_unauthorized` once the key is revoked or no longer grants `logs:read`. If the key cannot be verified for two minutes, the tail ends with code `tail_unverified`; open it again with the ID of its last event. The Loki WebSocket tail (`/v2/logs/loki/api/v1/tail`) still returns 501. - **CI/CD: checks a release waits on get machines first.** When an account's checks are short of machines, a pull request from the repository's own branches into the production branch gets them first, then a push to the production branch, each oldest first, then every other check. While one of these waits, the account's lower checks hand it machines as their running tasks end. A pull request from a fork is ordered with every other check. To skip pull requests into a branch whose commits are tested another way, such as a staging branch, list the branches to check in the production branch's config: `pull_requests: { branches: [main] }`. ## 30 October 2026 - **Sandboxes: `networkRules` in API v2.** `POST /v2/sandboxes` accepts `spec.networkRules` (`internet`, `allow`, `deny`, `connect`), the same rules as the v1 `network` field, enforced on every connection the sandbox opens. A rule that cannot be parsed returns 400 `invalid_network_rule` in v1 and reports the sandbox as `failed` in v2. Changing `networkRules` after a sandbox starts stops it with its disk kept; create a new sandbox to use different rules. Network rules on a copy of a snapshot are not supported yet: v1 returns 501 `feature_unavailable`, and v2 reports the sandbox as `failed`. A fork keeps its source's rules. - **CI/CD checks that run their setup start jobs immediately.** When a check runs its `setup` (its key has no warm snapshot yet), its first job now starts on that set-up machine as soon as the setup ends. Before, the machine was deleted once it was copied, and every job waited for machines started from the copy, which only one server held: when that server was full, a check could wait minutes with no job running. Machines for the rest of its work still start from the copy. - **Usage export shows the carry on each outbound traffic row.** In `GET /v1/usage/export`, each sandbox `egress` row's `outbound` measurements now include `carried`: the fraction of a microdollar the row started from, carried in from earlier outbound traffic in the same month, in bytes times microdollars per decimal GB. The row's charge plus `written_off_micros` equals `floor((carried + billable bytes × rate) / 1,000,000,000)`. Charges are unchanged. - **Agent sessions keep their task in the console after their history is archived.** A session's history moves to cold storage a week after the session ends. The console now still shows what the session was asked to do: the opening words of its first input. Before, the console showed no task for these sessions. Sessions archived before this change still show no task in the console; their full history is still available through the API. - **The MCP server accepts newer protocol versions.** A client that requests an MCP protocol version newer than the server supports, such as `2026-07-28`, now gets a working session at the server's newest version (`2025-11-25`). This applies to `initialize` and to the `MCP-Protocol-Version` header on later requests. Before, those later requests returned `400 Bad Request: Unsupported protocol version`, so clients such as Claude's connectors could not connect. A malformed version, or one older than the server supports, still returns 400. - **A sandbox has all its memory when its create returns.** `POST /v1/sandboxes` now returns only after the sandbox's guest holds all the memory it asked for (`memoryMiB`), so `/proc/meminfo` reports the full amount from the first command. Previously a sandbox could start with part of its memory and receive the rest during its first seconds. A sandbox created with a memory size other than the default can take longer to create, while the memory past its starting size is added. - **A job is no longer failed when its machine is paused by its host.** When a check's machine stops responding because its host paused it, the tasks it held now run on the check's other machines and do not count toward the three losses that end a job with "Lost 3 times, never reported". - **A check that times out while jobs wait for a machine reports "Could not run".** When a check's `timeout:` expires while every job that has not finished is still waiting for a machine that was refused for lack of capacity, those jobs now end with the reason `no machine came for its jobs in N minutes (...); re-run it once there is room`, and the check's GitHub run is titled "Could not run". Before, those jobs were reported as "Ran past its time limit", as if the code under test had hung. - **A sandbox's lease counts from when it first runs.** A paid sandbox that waits in `starting` for its server to have room now has its lease renewed while it waits, so it is no longer stopped with `time_limit` before it ever runs, and its server no longer refuses the start as `invalid_lease` once the wait used up its lease. No compute is charged for the time spent waiting. `diskKept` on a stopped sandbox is now `true` only when `POST /v1/sandboxes/{id}:restart` starts it from its disk: a stopped sandbox whose server was lost now returns `diskKept: false`. - **Evictable sandboxes.** A create or update can now set `evictable: true`. Runtime may then delete the sandbox once it has been paused or stopped for at least 15 minutes and its server needs the disk, the sandbox idle longest first. It is deleted as by `DELETE`; a running sandbox is never deleted for it. Use it for work you can recreate, such as build caches and CI machines. Sandboxes return the setting in `evictable`, and every SDK accepts it on create. ## 29 October 2026 - **A sandbox waiting on its disk is no longer paused as idle.** Idle pause now counts time a sandbox spends blocked on disk I/O as activity, alongside CPU, network traffic and open calls. Before, a sandbox whose processes were blocked on slow disk writes could use no CPU and be paused in the middle of its work. - **Sandboxes: `wakeOnTimers` turns off timer wakes.** A sandbox that pauses because it is idle wakes by itself when a process in it is due on a timer, such as a `sleep 600` loop or an agent that polls every ten minutes. This is on by default for every sandbox, and a timer wake is billed as ordinary use from the moment the sandbox runs. To keep request wakes and turn off timer wakes, set `wakeOnTimers: false` at create or with `POST /v1/sandboxes/{id}:update`; turning it off also cancels a timer wake already planned. The setting is in the TypeScript, Python, Go, Java and Ruby SDKs, the CLI (`--no-wake-on-timers`, `--wake-on-timers on|off`) and the MCP tools, and sandbox responses include it. See [timers wake an idle sandbox](./sandbox-environment#timers-wake-an-idle-sandbox). - **Free machine time in `GET /v1/usage` matches what a create is admitted on.** `trial.availableMs` now counts what running sandboxes hold and the free-usage rules, so a create without credit is refused with `included_usage_used_up` exactly when it is 0. The new `freeUsageHold` field says why an account has no free machine time (`hosting`, `region`, `card`, `withheld`, `no-source` or `unknown`) and what starts it; the refusal's `details.reason` names the same rule, and `npx withruntime usage` shows the message. Before, an account could read free time left while every create was refused. - **A wake that its server has no room for no longer stays in `resuming`.** If the sandbox's server refuses a wake for memory or disk room, the sandbox returns to `paused` within 30 seconds with its memory intact, and the wake operation reports `host_capacity` or `host_memory_pressure` with the reason. The sandbox can be woken again immediately, including by an automatic wake from a command. Previously the sandbox could stay in `resuming` until its lease ended, and every command returned 409 `sandbox_not_ready`. - **Accounts with credit keep their remaining trial hours.** An account that has paid or holds credit uses its remaining free trial hours before credit again, wherever it was opened and whether or not a card is saved. Since earlier today, such an account opened outside the 41 advanced economies, or opened by email with no card saved for auto top-up, was charged credit while it still had trial hours. - **Sandboxes report pressure stall information.** `/proc/pressure/cpu`, `/proc/pressure/memory` and `/proc/pressure/io` are now available in sandboxes started after this release, so tools that read Linux pressure stall information (PSI), such as `systemd-oomd` or a build's own throttling, work inside a sandbox. Sandboxes resumed from a snapshot taken before the release keep their previous kernel settings until they are restarted. - **Sandboxes: a timer wakes a sandbox that paused while idle.** When a sandbox pauses because it is idle, Runtime records when the next timer of a process in it is due (a `sleep`, a wait with a timeout, a `timerfd`, `alarm` or a POSIX timer) and wakes the sandbox about one second before then. Only timers due within 29 minutes of the pause, and pending for the whole idle time, count, so a process that wakes every few seconds does not keep waking it. A sandbox with no pending timer stays paused. A pause you request, a pause at the end of a time limit or of credit, and a sandbox with `autoWake: false` are not woken by timers. See [timers wake an idle sandbox](./sandbox-environment#timers-wake-an-idle-sandbox). - **CI/CD: test only the packages and crates a pull request touches.** Add `pull_requests: { tests: touched }` to the production branch's `runtime.yml`. A pull request into the production branch then runs only the tests of the packages (nearest `package.json`) and crates (nearest `Cargo.toml`) that hold its changed files, plus steps whose `inputs` it changes and steps that read every file. The other tests its change reaches are listed on the `left to main` job, and the production branch's check runs them after the merge. Turn on undo with it so that a commit that breaks the production branch is reverted automatically. A change to a root file other than Markdown, a file in a dot folder, a lockfile or toolchain file, a script a step runs, or the config file runs every test it reaches. The default, `tests: reached`, is unchanged. - **CI/CD: check only pull requests into the branches you list.** Add `pull_requests: { branches: [main] }` to the production branch's `runtime.yml`. A pull request into any other branch ends immediately, uses no machine, and its `runtime` run reads "Not checked" (action required, which never passes a required check). A pull request whose base branch changes is checked again. Without the list, every pull request is checked, as before. - **Free usage for accounts opened by email starts with a card.** An account opened by email, in the regions with free usage, starts its free usage once its owner adds a card under **Auto top-up** on **Usage & billing**. The card is checked by your bank and is not charged unless you turn on auto top-up. Each card starts free usage for one account. Accounts opened with Google and accounts that have bought credit are unchanged. See [free usage by region](./included-usage#free-usage-by-region). - **Stricter checks on new email sign-ups.** A sign-up code is not sent to an address at a domain registered less than 90 days ago, or to a fourth new address at the same domain within a day, unless the domain is a major email provider. Addresses that already have an account are not affected. Sign up with Google, or ask a teammate to invite you. - **One account per inbox when signing up by email.** A new address that is another form of an inbox that already has an account, such as a `+tag` or dotted Gmail address, is not sent a sign-up code. Sign in with the address the account was opened with, or with Google. - **Free machine time outside the listed regions starts with credit.** An account opened from a region outside the 41 countries in [free usage by region](./included-usage#free-usage-by-region) has no free machine time until it buys credit; buying credit provides the full monthly free usage in every region. Accounts opened from the 41 countries are unchanged. - **Free usage depends on the region an account was opened from.** An account that has never bought credit and was opened from one of 41 countries, including the United States, Canada, the United Kingdom, most of Western Europe, Japan and Australia, receives the full free machine time. An account opened from any other region receives 15 minutes of a default sandbox (2 vCPU, 4 GiB): 1 GiB-hour of machine memory and 0.5 vCPU-hours of active CPU. Buying credit provides the full monthly free usage in every region. Free saved copies and disks are the same in every region. See [free usage by region](./included-usage#free-usage-by-region). - **Lower network limits for sandboxes without credit.** A sandbox of an account without credit now uploads at up to 10 Mbit/s (was 50 Mbit/s) and transfers up to 1 GiB a day, inbound and outbound combined (was 5 GiB). Connections to cryptocurrency mining pools are blocked on every port. Downloads have no speed limit, and paid sandboxes are not affected. See [included usage](./included-usage#without-credit). - **A wake of a sandbox moving to another server returns 409 `sandbox_moving`; terminals reattach without errors.** A wake, or a call that wakes a paused sandbox, while the sandbox's paused memory and disk are still being copied to another server now returns `409 sandbox_moving` with `Retry-After`, instead of `503 no_capacity`. The sandbox is unchanged and stays paused. The JavaScript, Python, Go, Java and Ruby SDKs wait and retry it as they do `no_capacity`. A terminal opened with `processId` on a shell that another terminal already typed into no longer closes with `502 guest_failed` ("invalid input offset") on its first keystroke. Its input now starts where the shell's input is. - **Accounts that have never paid are suspended for mining, port scans or internet sweeps.** A sandbox of an account that has never paid that connects to a mining pool, scans ports, or connects to a large number of distinct destinations within a few minutes now has its network cut, and the account is suspended. Accounts that have paid are not affected. See [included usage](./included-usage#without-credit). - **Forks start on another server when the source's server is full.** `POST /v1/sandboxes/{id}:fork` now returns `503 no_capacity` only when no server in the region has room for the copies. When the source's server is full, each copy starts on another server in the region, and the snapshot is copied there first, including large snapshots. The fork's memory limit (a quarter of a server's free memory per fork) now applies to each server the copies can start on, not to the source's server alone. - **A sandbox that stops responding is reported within 5 seconds, and a restart no longer reports a kept disk as deleted.** A running sandbox that stops responding now returns `unreachable`, with `since` and `reason` (`agent_not_answering`, `disk_stalled` or `host_quarantined`), from `GET /v1/sandboxes/{id}`, and the new events `sandbox.unreachable` and `sandbox.reachable` are sent to webhooks. Previously such a sandbox returned `running` with no indication. When the sandbox's server cannot finish writing to its disk, a new command now returns `sandbox_unreachable` (503, retryable) after 5 seconds, with `details.reason` set to `disk_stalled`. Previously the request waited about 30 seconds and returned `guest_busy` (429). The request runs nothing, and commands already running continue. `POST /v1/sandboxes/{id}:restart` on a running sandbox whose server has not confirmed the reboot's stop now returns `sandbox_unreachable` (503, retryable) and keeps the disk. Previously it returned `not_resumable` (409) with `diskKept: false`. A retry with the same `Idempotency-Key` waits for the stop, then starts the sandbox from its disk. A command sent to a stopping sandbox now returns `sandbox_stopped` with "is stopping" instead of "is stopped (ended earlier)". When the server could not pause a sandbox to free disk space, the sandbox keeps running instead of refusing every call. See [troubleshooting](./troubleshooting#a-running-sandbox-refuses-commands). ## 28 October 2026 - **Usage export lists every running-disk charge, including amounts written off.** In `GET /v1/usage/export`, each sandbox `disk_used` row now covers all disk usage since the previous row, and its measurements include `carried`, the fraction of a microdollar carried in from the previous row. A running-disk charge that your balance, cost cap or spending limits could not cover now appears as a row with `chargedMicros` of `0` and its `written_off_micros`. Charges are unchanged. - **Woken sandboxes now end at their time limit.** A sandbox woken with `timeoutSeconds` now pauses or stops (as `onTimeout` specifies) exactly when that time limit ends. Previously, a sandbox woken this way and left to reach its limit could be woken again by Runtime and run, billed, up to one more `timeoutSeconds` past the limit. - **Go, Java and Ruby SDKs: terminals and tunnels stay open when the API restarts.** When the API closes a terminal or tunnel WebSocket with code 1012 (Service Restart), the Go, Java and Ruby SDKs now reconnect with the resume token immediately and continue from the exact byte where they stopped, with nothing lost or sent twice; input written during the hand-off is held and sent after the reconnect. Previously, these SDKs ended the terminal or tunnel. See [Resuming terminals and tunnels after a restart](/docs/api#resuming-terminals-and-tunnels-after-a-restart). - **Terminals, SSH sessions and port forwards stay open when the API restarts.** Previously, an API release closed every open terminal and tunnel. The API now moves each one to the instance that serves next: the server sends `{"type":"restart"}`, then a resume token, and closes the WebSocket with code 1012 (Service Restart). The JavaScript and Python SDKs, the CLI and the console reconnect with the token immediately and continue from the exact byte where they stopped, with nothing lost or sent twice. Clients that speak the WebSocket protocol directly can follow the same steps; see [Resuming terminals and tunnels after a restart](/docs/api#resuming-terminals-and-tunnels-after-a-restart). - **A request whose egress secret cannot be added returns 503 instead of sending the placeholder.** When a sandbox's server has not heard from Runtime's API within the last minute (for example, in the first seconds after a server update), it holds secret values back. Previously, an HTTPS connection opened during that time went to the destination without the secret, and every request on that connection carried the placeholder for as long as the connection stayed open, even after the server heard from the API again. Connections to a secret's host are now always handled by the step that adds the secret. A request that carries the placeholder while its value is held back returns `503 Service Unavailable` with `X-Runtime-Egress: secret-not-current` and `Retry-After: 1`, and the connection closes; the value is added again from the first request after the server hears from the API. Server updates also no longer hold secret values back. - **Commands keep running while a server prepares to pause a sandbox.** When a server paused a busy sandbox on its own (for example, when its disk ran short), it refused every command from the moment it decided to pause, which could be minutes before the sandbox stopped running, and `GET /v1/sandboxes/{id}` still returned `running` meanwhile. Commands are now accepted until the sandbox is paused, the pause is reported immediately, and the next command wakes the sandbox. If the server stops running a sandbox without reporting why, `exec` and file calls now return `503 sandbox_unreachable` (retryable, nothing ran) with a hint to restart it with `POST /v1/sandboxes/{id}:restart`, instead of `409 not_running` for a sandbox whose state is `running`. - **The audit log records your account's backup encryption key.** Each account's backups, snapshot copies and paused sandboxes stored off the server are encrypted with a key of its own. The audit log now records when Runtime creates that key, rotates it (the old key becomes read-only: it still opens existing backups and encrypts no new ones) and destroys an old key. A key is destroyed only after no backup, copy or replica still uses it. Entries are `account.storage_key_created`, `account.storage_key_rotated` and `account.storage_key_destroyed`. - **A command's output stream continues when its server pauses the sandbox.** When a server paused a sandbox on its own (to make room for another sandbox, or at the end of the sandbox's time limit) while a streamed `exec` or a process output read was in progress, the call returned `409 sandbox_paused` and the command's output was lost to the caller. The API now wakes the sandbox and resumes the call, and the command runs to its exit. This also applies to an `exec` sent with an `Idempotency-Key` (every SDK sends one), which is never run twice. A sandbox with `autoWake: false` still returns `409 sandbox_paused`. - **Usage for volumes, backups and snapshots.** `GET /v2/volumes/{id}:usage`, `GET /v2/backups/{id}:usage` and `GET /v2/snapshots/{id}:usage` return a resource's storage usage and charge for one calendar month (UTC), as `GET /v2/sandboxes/{id}:usage` does for a sandbox. The SDKs add the matching `usage` method to each. - **Volumes, backups and snapshots report their measured size.** In the v2 API, `status.usedBytes` on a volume, backup or snapshot is now measured every hour, and the new `status.measuredAt` field returns when it was measured. For a volume, `usedBytes` is the bytes written to it; for a backup or a snapshot, it is the bytes only that backup or snapshot stores, which is what its storage is billed on. Prices and billed quantities are unchanged. - **Sandboxes paused for more than an hour resume in under a second.** A paused sandbox's memory and disk now stay on its server until that server needs the room. Before, they were moved to off-site storage one hour after the pause, and a resume after that took 20 to 50 seconds while they were copied back. A sandbox whose server does need the room still resumes from its off-site copy, with its files and running programs intact. - **Stopping a sandbox keeps writes made just before the stop, even when its agent is busy.** When a sandbox whose disk is kept (or that has a volume attached read-write) is stopped, Runtime asks the guest to write its pending data to disk before the machine ends. If the guest's agent was briefly unable to take that request, for example while the sandbox was out of memory, the request was not retried, and data written in the last few seconds before the stop could be missing from the disk. The request is now retried until the guest takes it, for up to 12 seconds. The stop itself still returns immediately. - **Sites that block by address are paced per outbound address.** Each outbound address shared by more than one account now opens at most 150 new connections an hour to Google, Bing, Amazon and the other [sites that block by address](./egress#sites-that-block-by-address), for all accounts together, and at most 100 an hour for any one account. This includes the addresses kept for scrapers, which had no limit, so one account's traffic could get such an address blocked for every account sent from it. Past a limit, a connection returns `429 Too Many Requests` with `X-Runtime-Egress: rate-limited:guarded-site` and `Retry-After`. Before, one account could open 300 an hour from a shared address. A dedicated outbound address is not limited. - **A create whose sandbox is deleted while it starts returns 410 `deleted`.** If a sandbox is deleted before its own `POST /v1/sandboxes` returns, that create now returns 410 `deleted` with `details.sandboxId`. It previously returned 404 `not_found`. A create with `getOrCreate: true` whose name's holder is deleted while the create waits for it now creates a new sandbox instead of returning 404. See [names](./api#sandboxes). ## 27 October 2026 - **A job's runs in the v2 API and every SDK.** `GET /v2/jobs/{id}/runs` returns a job's runs, newest first and paginated, and `GET /v2/job-runs/{id}` returns one run: its execution, task, attempt, state, exit code and times. The SDKs read them with `jobs.runs(id)` and `jobs.run(id)` (`Runs` and `Run` in Go). - **Connections between your sandboxes are no longer reset in bursts.** Connections by name (`.sandbox.internal`) were refused past 100 new connections a second across the whole account, so a sandbox opening several at once saw some of them reset (about 17% with 8 at once). The limit is now 1,000 a second per account and 500 a second per target sandbox, and a connection over it waits for its turn instead of being refused. A sandbox created moments ago is also reachable within about a second; before, it could be refused with `private-network-off` or `private-network-no-sandbox` for up to 15 seconds. See [networking](./networking). - **OpenTelemetry exports in the v2 API.** The v2 API adds an `otel-exports` resource, with matching methods in the TypeScript, Python, Go, Java and Ruby SDKs. An export sets `endpoint` (an OTLP/HTTP base URL), `signals` (`logs`, `metrics`, `traces`) and `enabled`. `headers` are write-only: they are sent with every push and never returned; `status.headerNames` lists their names. An update that sends `headers` replaces all of them. A project can have at most three exports. The v1 OpenTelemetry export API returns the same responses as before. - **Image builds check variable names in the request schema.** A build request to `POST /v1/images` returns 400 `invalid_request` when a key of `env`, `recipe.env` or `buildArgs` is not a valid variable name (a letter or underscore, then letters, digits or underscores, up to 256 characters). `details.issues` names each such key. Builds already refused these names; the request is now refused before it is read further, and the published OpenAPI document declares the rule as `propertyNames`. - **Webhooks in the v2 API.** The v2 API adds a `webhooks` resource, with matching methods in the TypeScript, Python, Go, Java and Ruby SDKs. A webhook sets `url`, `events`, `enabled` and `description`. Increasing `spec.secretGeneration` by one rotates the signing secret; the previous secret keeps signing for `spec.keepPreviousSeconds`. The secret is returned only in the response to the create request and to each rotation. The v1 Webhooks and Events APIs return the same responses as before. - **Run virtual machines inside a sandbox with `/dev/kvm`.** New sandboxes on current-generation hosts boot a guest kernel with KVM and expose `/dev/kvm` to every user in the sandbox, so QEMU, Firecracker or an Android emulator runs with hardware acceleration instead of emulation. A VM inside a sandbox keeps running through pause and resume, and is carried into snapshots and forks. To check whether a sandbox has it, run `test -c /dev/kvm && echo yes`. Sandboxes created before this change keep their kernel until they are recreated. - **Waking a sandbox keeps the egress CA its running processes trust.** A sandbox started before 26 October 2026 and woken afterwards was given a new egress CA, and processes that were already running (Node.js with `NODE_EXTRA_CA_CERTS`, Bun, Python, Go) failed HTTPS requests that use egress secrets with `SELF_SIGNED_CERT_IN_CHAIN` until they were restarted. A wake now keeps the CA the sandbox already trusts and writes nothing to its trust store. See [security](./security). - **Accepting an invitation adds you only to the account that invited you.** A person whose first sign-in was through an invitation link was also given a separate account in their own name. Opening an invitation now creates nothing; you join the inviting account when you select Join. See [teams](./teams). - **Sandbox usage.** `GET /v2/sandboxes/{id}:usage?month=YYYY-MM` returns what a sandbox used in a calendar month (UTC): its metered periods, milliseconds running, vCPU-microseconds billed, MiB-milliseconds of memory, and the amount charged, in microdollars. Without `month`, it returns the current month. It requires the same permission as reading the sandbox, and is available as `sandboxes.usage(id, month)` in the TypeScript, Python, Go, Java and Ruby SDKs, as `runtime sandboxes usage [--month YYYY-MM]` in the CLI, and as an MCP tool. - **Volumes on v2 Apps, Functions and Containers.** `spec.volumes` lists up to 16 volumes, each with `volume`, `path` and `mode`. A volume is mounted at any absolute `path` outside the system folders (such as `/var/lib/postgresql/data`) before a copy's process starts. With `mode: write` (the default), one copy mounts the volume read-write, so the service runs one copy and `status.copyLimit` is 1, with `status.copyLimitReason` naming the volume. A new version, a rollback or a restart stops the old copy (`SIGTERM`, then `stopGraceSeconds`, default 30) and unmounts the volume before the new copy mounts it. With `mode: readMany`, each copy mounts its own read-only copy-on-write copy of the volume as it was when the version was made, and the service scales as usual. A volume another sandbox mounts read-write fails the version with `volume_attached`; deleting a service never deletes a volume. - **A sandbox at its memory limit stays reachable, and every process killed for memory is recorded.** Linux's out-of-memory killer now never ends Runtime's agent or the sandbox's network relay, so commands keep running after a kill. Each process it ends, inside a command or not, is recorded as the event `sandbox.oom_kill` with its name, process ID and memory. The event is shown on the sandbox's page in the console and sent to webhooks. When the agent does not answer, the API returns `sandbox_unreachable` (503, retryable), and names a recent out-of-memory kill when there was one. When the sandbox cannot read its environment, the API returns `sandbox_environment_unavailable` (503, retryable). Both replace `guest_busy`, which now means only that the sandbox is running as many commands as it can. `POST /v1/sandboxes/{id}:restart` on a running sandbox now reboots it from its disk instead of returning 409 `not_resumable`. See [troubleshooting](./troubleshooting#a-running-sandbox-refuses-commands). - **Relative file paths work in the compatibility layers, and Vercel's `stop()` sends one request.** Paths such as `./hello.txt` or `src/../a.txt` passed to the CodeSandbox, Cloudflare, E2B, Daytona and other compatibility layers now resolve against the layer's working folder (`/project/sandbox` for CodeSandbox), as the vendors' SDKs resolve them. Before, the API returned `400` with `invalid_request` for a path containing `.` or `..`. In `withruntime/vercel`, `sandbox.stop()` no longer reads the sandbox first: it pauses or stops it in one request, saving one round trip per stop. - **Volumes and backups in the v2 API; snapshots no longer pause the sandbox.** The v2 API adds `volumes` and `backups` resources, with matching methods in the TypeScript, Python, Go, Java and Ruby SDKs, the CLI and the MCP server. A volume sets `dailyBackups` and `backupRetentionDays`; a backup names its `volume` and reports `sizeMib` and `expiresAt`. A v2 snapshot now takes `mode` (`memory` or `disk`) and `retentionDays`, and reports `expiresAt`, `vcpus`, `memoryMib` and `diskMib`. Taking a snapshot of a running sandbox no longer pauses it: the sandbox keeps running while the snapshot is made. The v1 Volumes and Snapshots APIs return the same responses as before. - **Connections between your sandboxes are no longer held to upload limits.** Connections by name (`.sandbox.internal`) were limited to 100 Mbit/s per sandbox, and to 20 Mbit/s after the first 2 GiB, as uploads to the internet are. They are now limited only by each server's share of the private network. They do not count toward a sandbox's upload limits or its daily network amount. Each sandbox can still be reached for 100 GiB a day. See [networking](./networking). - **CI/CD jobs stay `queued` until a machine begins them.** A job is now `running` only once one of its machine's tasks starts, and `sandboxIds` lists each sandbox as soon as it begins the job, not only when the job ends. While a job is queued, its `reason` says what it waits for (for example, "Waiting for a machine: the check's machines are starting, or running other jobs' tasks"), the console shows how long it has waited, and the check's `runtime` run on GitHub says how many jobs are waiting and for how long. Previously a job could report `running` for many minutes while its tasks waited behind other jobs' tasks. - **Sandboxes in a compartment reach each other by name.** With the private network on, sandboxes created by keys in the same compartment can now connect to each other at `.sandbox.internal` or `.sandbox.internal`. Before, every connection from a compartment's sandbox was refused with `private-network-off`. Compartments stay walled: a sandbox reaches only sandboxes of its own compartment, or, outside every compartment, only the account's other sandboxes outside every compartment. A name across the wall returns `502` with `private-network-no-sandbox`. - **Requests through a sandbox's web proxy no longer wait 40 ms on responses.** The sandbox's web proxy relay (`127.0.0.1:10800`) and `guest-egress.py forward` now set `TCP_NODELAY` on the program's connection. Previously the kernel held the last segment of any response longer than one segment until the program acknowledged the one before, and most clients delay that acknowledgement by 40 ms. A call to Runtime's API from a sandbox that returns more than about 1.4 KB (creating, pausing or waking a sandbox, reading a file, listing sandboxes) completes about 40 ms sooner, as does any other request made through the proxy. A sandbox uses the new relay from its next boot, or from the wake after its next pause. The transparent relay, used by programs that ignore `HTTPS_PROXY`, gets the same change with the next base image. - **Speed figures are published only from live measurements.** The speed page, the guides, the website, llms.txt and the MCP server's instructions no longer quote the sandbox timings measured in September 2026. The [speed](./speed) page now lists only operations measured by the outside probe each minute whose own time is within 2× of their physical floor, with p50, p95, p99, sample counts and the raw samples. The disk, network and trace overhead measurements moved to [observability](./observability#what-metrics-and-traces-cost). - **Change a key's reach or daily limit over the API.** `PATCH /v1/keys/{id}` (`runtime.keys.update` in the TypeScript SDK, `runtime keys update` in the CLI) changes a key's `reach` (`agent` or `account`) or `dailyLimitMicros` (`null` removes the limit). A key can widen only the keys it made, never past its own reach or daily limit, and returns `403 forbidden` otherwise. It can narrow any key it sees, and it never changes itself. Each change is recorded in the audit log. Previously only the website could change an existing key. - **CI/CD reports a job lost with its machines as "Could not run".** When a job's task is lost on three machines without reporting a result, the job now ends as **Could not run** with the reason (for example, "Lost 3 times, never reported; the last on : no answer for 265 s"). Previously it was reported as "Failed in 0.0 s: exit code 1" and annotated on a file it never tested. A test failure in the same job still fails it. Lost runs are no longer recorded as test failures in flaky-test history. - **Sandboxes reach each other by name on every server.** A sandbox can now connect to another sandbox in its organization at `.sandbox.internal` whichever server runs either one; before, only sandboxes on one server could. Private networks (WireGuard) are reachable from sandboxes on every server too. Connections by name have a daily allowance of 100 GiB for each target sandbox, separate from the private network's 50 GiB. When the private network is briefly unavailable from a server, connections to it return `503` with `private-network-unavailable` and are never sent anywhere else; retry after a minute. - **The speed page is measured every minute, from outside, with the raw samples.** Runtime now times its operations once a minute (volumes every five minutes) through the public API, from a fixed location outside Runtime (a Cloudflare Durable Object in eastern North America), and reports Runtime's own time and the network's part separately, with p50, p95, p99 and sample counts for the last 24 hours. An operation is listed once its own time is within 2× of its physical floor, the least time its work can take on today's hardware. The summary is at `https://status.withruntime.com/speed.json` and each day's samples at `https://status.withruntime.com/speed/samples.jsonl?day=YYYY-MM-DD` ([speed](./speed)). - **Every API response includes a `Server-Timing` header.** The header, `Server-Timing: app;dur=12.3`, gives the time in milliseconds from when the API received the request until its response headers were ready. Subtract it from the time your client measured to get the network's share of each call ([the API](./api#responses-and-errors)). ## 26 October 2026 - **The CLI says when a newer version exists after it rejects an option.** An installed `withruntime` stays at its version until you update it, and `npx withruntime` runs an installed copy before it downloads one, so an older CLI could reject an option the docs list, such as `secrets scope --only-label`, with no sign that it was out of date. Now, when the CLI rejects an option or a command, it checks npm for the latest version, for at most 1.5 seconds, and prints the installed and latest versions with `npm install -g withruntime@latest`. It skips the check in CI and when `RUNTIME_NO_UPDATE_CHECK=1` is set ([the CLI](./cli)). - **Raising a job's timeout no longer requires changing its compute.** Previously, `PATCH /v1/jobs/{id}` with only a larger `timeoutSeconds` returned 400 with "Invalid integer" when the new timeout exceeded the job's `compute.durationSeconds`. Now the update extends `compute.durationSeconds` to the timeout plus 60 seconds of start time, up to 3,600, as a create does. A timeout that already fits leaves it as it is. Refusals of `timeoutSeconds` and `compute.durationSeconds` now name the field and the allowed range. - **HTTPS through egress secrets and preview links keeps working after a sandbox moves to another server.** Previously each server signed with its own certificate authority, so a process started before a wake, a move or a fork onto another server (such as Node with `NODE_EXTRA_CA_CERTS`) failed every request to an egress secret's host with `SELF_SIGNED_CERT_IN_CHAIN` until it restarted. Now each organization has one certificate authority, the same on every server, and running processes keep working. Each sandbox switches to its organization's certificate authority at its next start or wake; a process started before that switch must be restarted once. See [Security](./security). - **Sandboxes created from older images hold as many outbound connections as their limits allow.** Previously a sandbox created from an image or snapshot made before 15 October 2026 kept that image's web proxy relay, which held 64 connections and closed the 65th without a response, even when `GET /v1/sandboxes/{id}/network` reported a higher `limits.connections`. Now every sandbox boots with the current relay, so it holds its full limit and a connection past it returns `503 Service Unavailable` with the limit in `X-Runtime-Egress`. A sandbox that resumes saved memory (a wake, or a copy of a memory snapshot) takes the current relay immediately before its next pause, so it holds its full limit from the wake after that. See [The network](./sandbox-environment#the-network). - **Agents that start from a repository start their sessions.** An agent whose definition sets `from: {"repo": "owner/name"}` starts each session from that repository's CI/CD warm snapshot. Previously every session of such an agent ended immediately with `sandbox_refused` (403 `forbidden`, "a create naming another snapshot"), because the session's key was allowed to create its sandbox only from a snapshot the definition names by id. A session's key may now create its sandbox from any ready warm snapshot of the definition's repository, and from no other snapshot. - **A secret can now be given to the machine as its real value.** Some secrets are not HTTPS headers: a database password, an SSH key, a signing key or a service account's JSON file. Store one with `--exposed` (or `exposed: true` in the API, the MCP tool and the SDKs) and the machines it reaches get the value itself in its variable. It may be any text up to 64 KiB, several lines included. The value is still sealed, and opened only on the server that runs the machine. Code in the machine can read it, so the placeholder kind stays the default and the one to use for HTTPS APIs. For the same reason, it goes only to the sandboxes and Apps you name by id (`--only-sandbox`, `--only-app`), never to every sandbox or by label, and only a key that may run commands in each of them can store it. Which machines get it changes only when you store it again with its value. Every secret now has a version too, one more for each change to it: its value, who gets it, its hosts or its variable. Pass it as `expectedVersion` (`--expected-version`) and a write happens only if no one has changed the secret since; otherwise the answer is 409 `version_conflict`. `GET /v1/egress-secrets/{name}` and `runtime secrets get` show one secret's kind and version, never its value. The website's Secrets page offers both kinds and shows each secret's kind and version. See [a secret the machine needs itself](./security#a-secret-the-machine-needs-itself). - **A failed agent session shows why in the console.** When a session fails because its sandbox could not be created (`sandbox_refused`), the session's page in the console now shows the error the API returned, which previously appeared only in the session's events. - **App deploys report `live` only when the address serves them.** A deploy reaches `live` only after every router serves the new version's route, and an App's `state` is `waiting` until its first version is served, so `waitFor=live`, `runtime apps deploy` and the SDKs return when the address answers from the new version. - **CI/CD: built-in and light-sandbox steps record their result immediately.** An `undoes` or `hot-spots` step, a gate, or a `machine: light` step now shows its result in the console as soon as it finishes, instead of when the check's other jobs end. Previously such a step stayed "running" until then, and a check whose other jobs waited more than 40 minutes for capacity failed with "undoes sent nothing for 40 minutes" although the step had passed. - **`GET http://runtime.internal/v1/identity/token` returns 401 for a request token that is not the sandbox's current one.** It previously returned 403. Read the current token from `http://runtime.internal/v1/environment` and retry. The JavaScript and Python SDKs already do this for both statuses. - **Browsers start with Chromium already running.** A headless browser at the default window size (1280 by 800) is now created from a snapshot of Chromium that is already running, instead of starting Chromium in the new sandbox, which took 1 to 3 seconds. Each browser gets its own CDP secret, its own random state, and an empty profile. A server builds the snapshot the first time a browser of that size is created on it, so that first browser starts as before. Other settings start Chromium as before. - **An outbound connection that cannot reach Runtime's web proxy returns `503 Service Unavailable` with the reason.** Previously the sandbox's relay closed such a connection without a response, which clients reported as `Connection reset by peer`. Now the response carries `X-Runtime-Egress: proxy unreachable ()` and `Retry-After: 1`. A sandbox holds about 1,000 outbound connections at the same time, fewer than `limits.connections` for a sandbox with more than one vCPU, until a later release raises it; past that, new connections return this response. - **A sandbox with no time limit can be funded up to 12 hours ahead.** Set `leaseSeconds` (from its first lease, 1,800 seconds, to 43,200) on create or `:update` (`--lease` in the CLI, `lease_seconds=` in Python, `.leaseSeconds()` in Java, `LeaseSeconds` in Go), and each renewal funds it that far ahead instead of 1,800 seconds. It is renewed when half is left, so it keeps running for at least half of `leaseSeconds` if Runtime's control plane cannot renew it. Each renewal holds that much time's credit at the sandbox's rates; what it does not use is released. A sandbox on the free usage renews by 1,800 seconds until the account has credit. Only an owner's, an admin's or an account-wide key may set it, on a sandbox one of those made. If that key or member loses the right, a sandbox funded past its first lease is paused or stopped at once, as its lease's end would. A sandbox's details include its `leaseSeconds`. - **CI/CD: a cancelled check names the key that cancelled it, and your production branch's check gets machines sooner.** A check cancelled with `POST /v1/ci/checks/{id}:cancel`, `runtime ci cancel` or the MCP tool now shows the reason `asked through Runtime's API with the key "