OpenBrowse

Troubleshooting

Fixes for the failures that actually happen: Chromium not launching, a blank VNC view, sessions stopping early, and updates that will not install.

The live view page covers reading the step feed, which diagnoses most failed runs directly. This page covers the rest: the server, the display stack, and the API's error responses.

Chromium will not start

Symptom: sessions fail immediately with a Chromium launch error.

Almost always a missing shared library. Install the full dependency set:

playwright install-deps

Then check that the browser binary actually resolves:

python3 -c "import cloakbrowser; print(cloakbrowser.ensure_binary())"

If the path it prints does not exist, reinstall:

pip install --force-reinstall cloakbrowser

The live view is blank

Symptom: the browser panel in the dashboard loads but shows nothing.

First, give it a moment. Only the virtual display starts with the session; the VNC server and the websocket bridge spawn on your first request to view it, so the first frame of a view you have just opened arrives a beat after the panel does.

If it stays blank, the live view needs three separate processes per session: a virtual display, a VNC server, and a websocket bridge. Confirm all three are installed and on $PATH:

which Xvfb x11vnc websockify

Install anything missing:

sudo apt install -y xvfb x11vnc novnc websockify

Then check they started cleanly:

journalctl -u openbrowse -n 50

Authentication errors

401 with Server authentication is not configured: the instance has no API_KEY. Set one in .env (or run the /setup screen on a fresh install) and restart.

401 with Invalid API key: the key does not match. The server accepts it either as Authorization: Bearer <key> or in the X-Browser-Use-API-Key header; check which one your client sends and that no proxy strips it.

Requests suddenly rejected after repeated failures, with a 429 and a Retry-After header: failed attempts are throttled per client IP with a doubling lockout (five free failures, then 1s doubling up to 15 minutes). Wait, or restart the server to clear the in-memory state, then fix the key rather than retrying it.

The dashboard sends you to /setup instead of asking for a password: the instance has neither a dashboard password nor an API key, so there is nothing yet that a password could be checked against. It answers with a 303 to the setup wizard rather than a challenge, because a challenge on a fresh install is one nothing could answer. Finish the wizard and restart; from then on the dashboard asks for the credentials you chose.

No default password ships, and this is why. A known pair would be a working credential during exactly the window before anyone has chosen one, on a service holding provider API keys and imported profiles with live logged-in cookies. The v3 API is deliberately not part of this: a programmatic call still fails closed with a 401 rather than being redirected to an HTML wizard.

The API refuses my request

422 with Session is running, not idle: you sent a follow-up task (sessionId set) to a session that has not finished. Poll until it is idle, or stop it first with strategy task. A keepAlive session is exempt from the status rule, because it is a conversation rather than a one-shot run, but it is still refused while it is genuinely mid-task.

422 with Task is required when targeting an existing session: a follow-up request must carry a task.

422 naming reasoningEffort: the value is not valid for that model; the error lists the accepted set. The per-model table is in choosing a model. The same applies to the retired thinkingEffort and modelThinkingEffort fields, and to sending both thinkingLevel and reasoningEffort at once.

'x' is not a valid model: the model name is not in the supported list, also in choosing a model.

A session goes straight to error with Profile is in use by running session <id>: two runs cannot hold the same browser profile at once. The claim is taken inside the run rather than at request time, so the second session is accepted and reaches running before it fails. Wait for the holder to finish, or give the second run its own profile.

Session errors at launch with needs OPENAI_API_KEY (or ANTHROPIC_API_KEY): the model's provider key is missing from .env.

A session stopped before finishing

Read failureKind on the session first. It says in one word whether the failure was yours, the site's, or the model provider's, which decides whether retrying the identical request is worth anything:

failureKindRetrying the same request
provider_rate_limit, provider_server_error, provider_connection_error, provider_timeoutWorth it. Nothing about your task or the site caused this; failureStatusCode carries the provider's status
session_timeoutOnly on a quieter host, or with a tighter task
budget_exceededOnly with a higher cap; see cost control
invalid_outputNot as-is. The model's reply was truncated or unusable; a simpler schema usually fixes it
agent_failureNot as-is. Either the agent gave up, or the run never started at all: a missing provider key lands here too, before a single step runs. Read the feed to tell which
null, on a session that failedNot classified. A server restart or a profile another session already holds fails outside the classified path; lastStepSummary and the feed are the source of truth

failureKind is not the verdict on its own. A run stopped by its cost cap that salvaged a complete, valid answer store is recorded successful and still carries budget_exceeded, so read isTaskSuccessful alongside it.

Then check the last feed entry, or the lastStepSummary on the session. All four below reach lastStepSummary; the cost stop and the step timeout are additionally written into the feed as rows, so you will see those either way:

  • Stopped: Cost $X exceeded budget $Y: the maxCostUsd cap fired. Whatever the answer store held at that point is preserved in output, and the run is recorded a success only if that output validates and passes the completeness gate, so a partial result is visible rather than lost. The session goes stopped, or idle if it was created with keepAlive. Raise the cap, or reduce spend; see cost control.
  • Interrupted by server restart: the server went down mid-run. The session is marked error at the next startup because it can never resume; run the task again.
  • Status expired: the session was created without a task and never given one; task-less created sessions are expired after 15 minutes. Create a new one.
  • Step timed out and was cancelled before completing: one step exceeded the 520-second ceiling; the run continues past it, but repeated timeouts usually mean the host is overloaded.

Out of memory

Symptom: sessions are killed mid-task, the machine becomes unresponsive, or the OOM killer fires.

Each Chromium instance uses roughly 400 to 600MB. Budget 2GB per concurrent session to leave room for the pages it loads, the virtual display and the Python process, so concurrency is bounded by RAM rather than by anything in the application.

free -h
dmesg | grep -i oom

Reduce concurrency in .env and restart:

MAX_CONCURRENT_SESSIONS=1

The default is 1; if sessions are being killed at that setting, something else on the box is taking the memory.

Before reaching for swap, try the lighter browser profile, which lowers the per-session memory floor and is close to free on a machine with no real GPU:

CHROME_LIGHT_FLAGS=1

openbrowse tune --share most additionally caps the service's memory through systemd, so a runaway session is bounded rather than taking the machine down with it. Both are covered under sizing it for your machine.

If you would rather trade speed for headroom, add swap:

sudo dphys-swapfile swapoff
sudo nano /etc/dphys-swapfile   # set CONF_SWAPSIZE=4096
sudo dphys-swapfile setup
sudo dphys-swapfile swapon

Swap on an SD card is slow and will wear it. On a Raspberry Pi, prefer an SSD or lower concurrency.

Related: the server samples host CPU pressure and posts a system warning into a run's feed when it launches under saturation, because an overloaded host misses the timing windows in which embedded panels attach. If a run's failures coincide with that warning, re-run when the box is quiet before concluding the site changed. Where the kernel exposes pressure stall information that reading is stall time rather than load average, which is markedly the better signal; on a Raspberry Pi PSI is compiled out by default and host_tune.sh adds the boot flag that enables it.

The dashboard's tuning or restart buttons do nothing

Symptom: a machine that was tuned once no longer responds to the Settings page's tuning button, or a restart from the dashboard drops the process instead of restarting the service cleanly.

Both of those work through a sudoers entry, and that entry names the tuning script by its full path. An upgrade moves the package, so the path changes and the grant stops matching. The Settings page detects the mismatch and names the command, but the fix is the same either way:

openbrowse tune --share most

Run it after every upgrade. It rewrites the grant for the new path, covering both the tuning script and the systemctl restart the dashboard's restart button needs.

An update will not install

Symptom: the Settings page reports the update as failed, or openbrowse update exits non-zero.

If it refused rather than failed, a session was running: the install is deliberately blocked while any browser is live. Wait for the session to finish, or stop it, and try again.

Otherwise the upgrade command itself failed, and which command that was depends on which manager owns this copy: uv tool upgrade openbrowse, pipx upgrade openbrowse, pip install --upgrade openbrowse inside its virtual environment, or git pull --ff-only for a checkout. Running it by hand shows you the real error, which for a checkout is usually a local commit or a dirty tree that a fast-forward cannot pass. Restart afterwards.

If instead it names an install method but offers no command, the manager that owns this copy is not on the server's PATH. That is common under systemd, which hands the service a minimal PATH that often omits ~/.local/bin, where both uv and pipx install themselves. Upgrade from a shell instead. The same message appears for a copy in the system Python, where no unattended upgrade is safe.

If the badge never appears at all, UPDATE_CHECK_HOURS may be 0, which switches the background check off. openbrowse check-update asks immediately regardless.

Tailscale Funnel is not answering

Symptom: the public HTTPS URL returns a connection error or a 502.

Check the funnel is actually up:

tailscale funnel status

If that is empty, bring it back:

sudo tailscale funnel --bg 8420

Confirm the server is listening on the port the funnel points at:

ss -tlnp | grep 8420

And check the obvious one: Funnel is HTTPS only. An http:// URL will not work.

An extraction returned fewer records than the page shows

This is usually correct behaviour rather than a bug. Values without on-page evidence are refused at the answer-store boundary, and a field the source genuinely never displays is settled as absent instead of guessed, with the reason recorded in the feed; see structured output.

If you are confident the data is on the page, the usual causes, in order:

  • The listing is inside a cross-origin iframe that never attached. This is handled automatically, but check the feed for a find_links entry reporting 0 link(s) matched or a frame filter matching 0 frames, and for read_pages shell-read retries that did not recover.
  • The content is behind unusual lazy loading. The link collector scrolls until the count is stable, but a page with a non-scroll loading trigger (a button, a filter) may need the prompt to say so explicitly.
  • The prompt did not state a completion target. If the page shows a total, say so: "keep going until your count matches the displayed total" is a materially better instruction than "get all of them". See writing tasks.

Export the run (full scope) from the dashboard when you want to attach it to an issue; it contains every action, error and reviewer note.

On this page