# How to run a browser automation agent in a cloud sandbox Give the agent a Linux desktop in a sandbox, send it screenshots, and apply the clicks and typing it chooses. **On Runtime the browser runs on a machine of its own and you pay for the CPU it uses.** A 2 vCPU, 4 GiB sandbox costs $0.03125 an hour while the agent waits on its model and $0.08 an hour with both CPUs busy. The desktop takes clicks, typing and screenshots through the SDK, streams live to a private link, and records to MP4, with nothing to install on your side. ## The short answer Start a desktop, open the page, then loop: screenshot to the model, action back to the desktop. ```ts check import { Sandbox } from "withruntime"; type Action = | { kind: "click"; x: number; y: number } | { kind: "type"; text: string } | { kind: "open"; url: string } | { kind: "done" }; type Model = (screenshot: Uint8Array) => Promise; // your computer-use model export async function browse(model: Model, startUrl: string, maxSteps = 30) { await using sbx = await Sandbox.create({ diskMiB: 8192, timeoutSeconds: 1800 }); const { streamUrl } = await sbx.desktop.start({ width: 1280, height: 800 }); console.log("watch it live:", streamUrl); // private: it carries a one-time token await sbx.desktop.open(startUrl); for (let step = 0; step < maxSteps; step++) { const action = await model(await sbx.desktop.screenshot()); if (action.kind === "done") break; if (action.kind === "click") await sbx.desktop.click(action.x, action.y); if (action.kind === "type") await sbx.desktop.type(action.text); if (action.kind === "open") await sbx.desktop.open(action.url); } return sbx.desktop.screenshot(); // the final state } ``` ```python check from withruntime import Sandbox def browse(model, start_url: str, max_steps: int = 30) -> bytes: """model(png_bytes) returns a dict: {"kind": "click", "x": .., "y": ..}, {"kind": "type", "text": ..}, {"kind": "open", "url": ..} or {"kind": "done"}.""" with Sandbox.create(disk_mib=8192, timeout_seconds=1800) as sbx: desktop = sbx.desktop.start(width=1280, height=800) print("watch it live:", desktop["streamUrl"]) sbx.desktop.open(start_url) for _ in range(max_steps): action = model(sbx.desktop.screenshot()) if action["kind"] == "done": break if action["kind"] == "click": sbx.desktop.click(action["x"], action["y"]) elif action["kind"] == "type": sbx.desktop.type(action["text"]) elif action["kind"] == "open": sbx.desktop.open(action["url"]) return sbx.desktop.screenshot() ``` The first start in a sandbox installs the desktop, about 90 seconds and 1 GB of its disk, once; Firefox follows in the background, and an `open` before it is ready waits for it. That is why the sample asks for an 8 GiB disk. ## Three ways to drive a browser | Way | What the agent sees and does | Best for | | ---------------------------- | ------------------------------------------- | --------------------------------------- | | The desktop | Screenshots in; clicks, typing and URLs out | Computer-use models; any site or app | | Playwright MCP server | Page structure and browser tools over MCP | Agents that already speak MCP | | Playwright or Puppeteer code | Your script, run with `exec` | Known flows, scraping, end-to-end tests | Start the Playwright MCP server inside the sandbox with one call, and give your agent its URL and headers: ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime(); await using sbx = await runtime.sandboxes.create({ diskMiB: 8192 }); await sbx.mcp.start([{ id: "playwright" }]); const gateway = await sbx.mcp.ready(); for (const server of gateway.servers) console.log(server.name, server.url); console.log(gateway.headers); // send these with every request ``` `runtime.mcp.catalog()` lists every server with its licence and pinned version. For scripted Playwright, see [Playwright in a cloud sandbox](/integrations/playwright). ## Watch it and keep a record The live view is a private preview of the desktop, so a person can watch the agent and step in. A recording saves the session as MP4: ```ts check import { writeFile } from "node:fs/promises"; import { Sandbox } from "withruntime"; await using sbx = await Sandbox.create({ diskMiB: 8192 }); await sbx.desktop.start(); const rec = await sbx.desktop.recordings.start({ fps: 10, maxMiB: 256 }); await sbx.desktop.open("https://example.com"); await sbx.desktop.recordings.stop(rec.id); await writeFile("session.mp4", await sbx.desktop.recordings.download(rec.id)); ``` ```python check from withruntime import Sandbox with Sandbox.create(disk_mib=8192) as sbx: sbx.desktop.start() recording = sbx.desktop.recordings.start(fps=10, max_mib=256) sbx.desktop.open("https://example.com") sbx.desktop.recordings.stop(recording["id"]) with open("session.mp4", "wb") as file: file.write(sbx.desktop.recordings.download(recording["id"])) ``` A recording uses one encoder thread at low priority and at most 4 Mbit/s, so the browser comes first. It never grows past `maxMiB`. ## Keep the agent on the sites it should visit Once the desktop and browser are in, narrow the network to the sites the task needs. The rule applies at once, binds root inside the sandbox, and is enforced on the host: ```ts check import { Sandbox } from "withruntime"; await using sbx = await Sandbox.create({ diskMiB: 8192 }); await sbx.desktop.start(); await sbx.desktop.open("https://example.com"); await sbx.network.set({ internet: true, allow: ["example.com", "*.example.com"] }); ``` To give the agent an API it should call, store the key as a [secret](/docs/security#secrets-sandboxes-never-see): the sandbox holds only a placeholder, and the host's proxy adds the value on HTTPS requests to the hosts you name ([egress control](/glossary/egress-control)). ## Start ready Install the desktop once, keep that sandbox as a snapshot, and start every task from it. The copy starts with the desktop installed and running: ```ts check import { Runtime } from "withruntime"; const runtime = new Runtime(); const base = await runtime.sandboxes.create({ diskMiB: 8192 }); await base.desktop.start({ width: 1280, height: 800 }); await base.desktop.open("https://example.com"); const snapshot = await base.snapshot({ name: "desktop-ready", retentionDays: 30 }); await base.stop(); await using task = await runtime.sandboxes.create({ snapshot: snapshot.id }); await task.desktop.open("https://example.com"); ``` ## What a browser agent needs | Need | How Runtime covers it | | ---------------------------- | --------------------------------------------------------------------------- | | A real browser and screen | A Linux desktop with Firefox; clicks, typing, screenshots | | A person watching | A private live stream link | | An audit trail | MP4 recordings, capped by `maxMiB` | | One task per browser profile | A Firecracker microVM per sandbox: no shared cookies, downloads or sessions | | Sites the agent may visit | Allow and deny lists, enforced on the host | | Many sessions at once | 100 sandboxes at once on a paid account to start | | No setup per task | Start from a snapshot with the desktop already running | ## What it costs Take 500 browser tasks a month. Each keeps a 2 vCPU, 4 GiB sandbox running for 3 minutes, with the browser using 0.8 of a vCPU on average: ``` CPU: 500 × 3 min / 60 × 0.8 vCPU × $0.025 = $0.50 Memory: 500 × 3 min / 60 × 4 GiB × $0.0075 = $0.75 Total: $1.25 ``` Runtime charges $0.025 per vCPU-hour of measured CPU and $0.0075 per GiB-hour of memory ([pricing](/docs/pricing)). A 1 GB snapshot to start from adds $0.08 a month. New accounts get 50 free sandbox hours, no card; the trial reaches ports 80 and 443, which is what browsing needs. ## Start ```bash no-run npx withruntime sandbox run --trial --keep -- echo ready ``` The first run prints a link to approve in your browser. Then `runtime sandbox desktop start` prints a link to watch it ([CLI](/docs/cli)). Related: [Playwright in a cloud sandbox](/integrations/playwright), [coding agent sandbox](/use-cases/coding-agent-sandbox), [egress control](/glossary/egress-control), [sandbox snapshots](/glossary/sandbox-snapshot). Facts on this page were checked on 25 September 2026.