Migrating from Browser Use Cloud
What changes when a browser-use-sdk integration moves from Browser Use Cloud to self-hosted OpenBrowse: the two-line client change, and everything else.
OpenBrowse serves the same v3 REST surface as Browser Use Cloud, so the client change is two lines. This page is about everything else: the handful of places where the two are not the same, ordered by how likely each one is to cost you an afternoon. If you are still deciding rather than migrating, the dimension-by-dimension comparison covers what you gain and what you give up.
The two lines
import { BrowserUse } from "browser-use-sdk/v3";
const client = new BrowserUse({
apiKey: process.env.OPENBROWSE_API_KEY,
baseUrl: "https://your-host/v3",
});The import matters. The bare browser-use-sdk entry point is the v4 client, whose runs API has a different request shape that OpenBrowse does not implement. The v3 client lives at browser-use-sdk/v3 and its method is sessions.create, not tasks.create.
The baseUrl needs the /v3 suffix, because the SDK's own default base URL ends in /api/v3 and its request paths are relative to it.
Retry logic, polling, profile ids, output schemas and cost caps all carry over untouched.
1. There is no proxy
Browser Use Cloud runs a managed US residential proxy by default, so target sites see a residential IP. OpenBrowse has no proxy layer at all: requests leave from your machine's own address.
For most people that is a saving rather than a loss. A box on your home broadband already has a residential IP, which is the thing the cloud's proxy exists to give you, and residential egress is one of the more expensive parts of what you are paying a hosted runner for. Self-hosting on a Pi at home gets it as a side effect of where the machine is, at no extra cost and with no per-gigabyte meter running.
The case where it genuinely bites is a VPS. A datacentre IP is exactly what sites blocking automated traffic look for first, and moving from the cloud's residential pool onto one is the change most likely to alter your results.
Whichever you run on, this is the difference that changes results without erroring. Nothing throws; the pages simply come back different. Test anything geo-gated, rate-limited by IP, or fussy about datacentre ranges before you migrate the rest.
proxyCountryCode is accepted so your code still compiles, and does nothing: there is no per-session proxy selection to reach for. If you do need one, put it in front of the instance at the network level, or keep those particular jobs on the cloud. Note that geo-targeting is the one thing a home connection cannot substitute for, since your IP is wherever you are.
2. Ten request fields are accepted and ignored
Three of them are declared in the request model and deliberately inert:
| Field | What happens |
|---|---|
proxyCountryCode | Ignored, as above |
enableRecording | Ignored. Sessions are not recorded and recordingUrls comes back empty |
skills | Ignored. There is no skills or marketplace equivalent |
The other seven are not declared at all, so they are dropped silently before your request is ever validated:
| Field | Why it matters |
|---|---|
agentmail | The cloud defaults this to on. A caller expecting a provisioned inbox gets none, and no error |
codeMode | Changes the shape of output on the cloud to {text, code}. Here output keeps its normal shape |
workspaceId | No workspaces exist. workspaceId comes back null |
enableScheduledTasks | No scheduler. Schedule from your own side |
cacheScript | No script cache |
autoHeal | No auto-heal behaviour |
useOwnKey | Always effectively true: the instance uses the provider keys in its own .env |
agentmail and codeMode are the two worth checking for in your codebase, because both change what you get back rather than merely doing less.
The matching response fields (recordingUrls, screenshotUrl, workspaceId, proxyCountryCode) are always empty or null.
3. Fourteen of the cloud's model names are rejected
Browser Use Cloud's v3 model enum has 22 names. OpenBrowse implements 8 of them. Naming one of the other 14 fails at request time with a 422 rather than falling back to a default.
The cloud's own default, claude-opus-4.7, is supported here. It is not the default here, though, and that catches people the other way round:
Omit model on the cloud and you get claude-opus-4.7. Omit it here and you get whatever the instance's DEFAULT_MODEL names, or, where that is unset, the default for whichever provider the instance has a key for, silently and without an error. On an OpenAI-configured instance that is a swap of provider as well as model, and it takes the reasoning depth with it either way. So a caller who names a model risks a 422, and a caller who relied on the cloud's default gets a quiet substitution instead. Set model explicitly and neither happens.
Supported on both: claude-opus-4.7, claude-opus-4.8, claude-opus-4.6, claude-sonnet-5, claude-sonnet-4.6, gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna.
Rejected here, with the nearest equivalent:
| Cloud name | Here |
|---|---|
bu-mini, bu-max, bu-ultra | No equivalent. These are tiers rather than models; pick a model by name |
gemini-3-flash, gemini-3-pro, gemini-3.1-pro, gemini-3.5-flash | Google is not supported yet |
gpt-5.2, gpt-5.5, gpt-5-mini, gpt-5.4-mini | Use gpt-5.6-sol, gpt-5.6-terra or gpt-5.6-luna |
claude-haiku-4.5 | Use claude-sonnet-4.6 |
glm-5.2, minimax-m3 | No equivalent |
OpenBrowse also serves models the cloud's v3 enum does not offer: claude-opus-5, claude-fable-5, claude-mythos-5, and a [1m] long-context variant of every Anthropic name. Of those, the cloud reaches claude-opus-5 and claude-fable-5 through its v4 runs API only, and has no equivalent of claude-mythos-5 or the [1m] variants at all. The full list is on choosing a model.
4. thinkingLevel: null is rejected
The cloud's thinkingLevel field works here and maps onto reasoningEffort, with disabled becoming none. One exception: the cloud documents thinkingLevel: null as the way to clear the setting, and here an explicit null is read as a value rather than as an absence, so it fails with 'None' is not a supported thinkingLevel.
Omit the field entirely instead.
5. liveUrl is not immediate
On Browser Use Cloud, liveUrl is available the moment a session is created. Here, creating a session returns status: "created" with liveUrl still null; the instance then allocates a virtual display and launches Chromium, and only then does the status become running with liveUrl populated.
That is a few seconds. If your integration reads liveUrl straight off the create response and embeds it, it will embed nothing. Poll until status is running.
6. The status sets are not identical
A session created without a task and never given one is expired after fifteen minutes and moves to expired, which is not in the cloud's status enum. If your code switches exhaustively over the cloud's set, add a branch for it.
timed_out exists on both, but reaches you differently: here it means the run exceeded its own time limit, and a run stopped by its cost cap ends as stopped rather than timing out, or as idle if it was created with keepAlive.
7. Two response fields the cloud does not have
A failed session here carries failureKind and failureStatusCode, which the cloud's v3 response model does not define. They distinguish a provider rate limit, a provider 5xx, a connection error and a provider timeout from a session timeout, invalid output, budget exhaustion and ordinary agent failure, so a retrying caller can tell a transient blip apart from an agent that will fail the same way again. Both are null on success.
They are additive, so a client that ignores them behaves exactly as it did. See how OpenBrowse works for the full set of values.
8. There is nothing else running the machine
The cloud absorbs uptime and capacity. Here a box has to stay up, and concurrency is bounded by memory: budget roughly 2GB of RAM per concurrent session. Installation covers openbrowse start, which registers it as a service so it survives a reboot, and exposing it safely covers reaching it from outside your network.
Upgrades are the part that comes closest to the managed experience: the instance watches PyPI for new releases and installs one on a click from its own Settings page, refusing while a session is running. That is still your click rather than someone else's, and it is covered here.
What does carry over
Worth stating plainly, because the list above is longer than the list of things that break:
- The whole
sessionssurface: create, get, list, stop, and follow-up tasks against an existing session. outputSchemaand structured output, with stricter validation here than on the cloud.maxCostUsd, enforced as a hard stop-loss, though on akeepAlivesession it is the pot for one dispatch rather than for the conversation; cost control sets out how it tops up. If your caps were priced for a service charging platform fees on top of tokens,CLOUD_MAX_COST_FACTORscales incoming caps down without touching your clients; see cost control.- Profiles. OpenBrowse imports the Playwright storage-state format a cloud profile export gives you, cookies plus per-origin
localStorage, and an imported profile keeps its cloud id, so existingprofileIdreferences keep working. See profiles. - Both authentication styles:
Authorization: Bearer <key>and theX-Browser-Use-API-Keyheader the SDK sends.
The openbrowse command
Every openbrowse subcommand: running the server, managing the systemd service, checking for and installing updates, tuning the host, and removing it again.
Writing tasks
How the agent reads your prompt, and the prompt shapes that measurably change what comes back: completion targets, record definitions, and scope.