VeilBrowserDocs

Troubleshooting

Diagnose launch refusals, unreachable workers, certificate failures and registry pull errors.

Symptoms grouped by where they surface, each with the underlying cause and the fix.

Launches are refused

402 with reason: "concurrency_limit"

The organization is already at its concurrency ceiling. Concurrency is counted organization-wide across every worker, not per host, so a second cluster does not get its own allowance.

{ "allowed": false, "reason": "concurrency_limit", "current": 200, "limit": 200 }

Check what is actually running

Open Launches in the dashboard, or call GET /v1/sessions?status=running. Compare the count against profileLimit from GET /v1/profiles/stats.

Look for leaked sessions

The usual cause is sessions that were never closed. A crashed automation process leaves its row running because nothing sent the stop. Kill them from the dashboard or with DELETE /sessions/:id on the owning worker.

Prevent recurrence

Set VEIL_AUTO_CLOSE=true so a browser closes when its last CDP client disconnects, and VEIL_IDLE_BROWSER_TIMEOUT to reap sessions that stop receiving traffic. Both are off by default.

Launches fail when the cloud is unreachable

This is intentional. Authorization is fail-closed: a worker that cannot reach POST /v1/license/authorize does not launch. Sessions already running are never killed.

Check SAAS_API_URL on the worker, then confirm outbound HTTPS to api.veilbrowser.net is permitted. Workers need egress to the control plane even in an otherwise isolated network.

AUTHENTICATION_ERROR on authorize

The worker's VEIL_KEY is wrong, disabled, or belongs to a different organization. Verify it against Settings → API keys, remembering that a deleted key cannot be restored — issue a new one and roll the fleet.

RESOURCE_NOT_FOUND naming a browser version

The profile asks for a build that worker does not have on disk. The error's constraint lists what it does have:

{ "field": "browserVersion", "code": "RESOURCE_NOT_FOUND",
  "constraint": { "availableVersions": [135, 141, 147] } }

Open the host page in the console, find Available to install, and install the missing build. If it is missing from the whole fleet, publish and activate it first — see Operating the cluster.

No profile matches the criteria

A launch spec whose os, osVersion, renderer or model matches no bundle fails before the browser starts. Query the worker for what it can actually produce:

terminal
curl -s "http://localhost:3000/profiles?os=win&browserVersion=147" \
  -H "X-API-Key: $WORKER_API_KEY"

Broaden the criteria, or publish a bundle that satisfies them. See Fingerprints.

Workers look wrong in the fleet view

Workers are polled over HTTP every 30 seconds.

StateMeaningWhere to look
HealthyReachable, status endpoint returned OK
API offlineHost reachable, Local API down or misconfiguredContainer logs; a bad API_KEY or missing VEIL_INSTALL_DIR exits at startup
Host downUnreachable entirelyFirewall, reboot, or the shared Docker network

Installs are refused against a host that is down, which is deliberate — a half-applied install is worse than a deferred one.

A worker is healthy but never receives launches

Three things to check, in order:

  1. clusterProxyEnabled is set on the Cluster row, and you have run Render & Reload since setting it. Until then the edge has no route to the worker.
  2. VEIL_WORKER_ID on the worker matches its FleetHost hostname exactly. Sticky /w/<workerId>/… routing fails silently when they diverge.
  3. VEIL_MAX_BROWSERS has not been reached. At the cap the worker returns 503 before the WebSocket upgrade so the load balancer retries the next one — which looks like the worker being skipped.

Sessions vanish after an update

Worker updates drain first and wait for running browsers to finish. If sessions are dying mid-update, the drain is being bypassed — check that the host has an updater agent registered rather than being recreated by hand with docker compose up -d.

Certificate issuance fails

The wizard reports which check failed. In order of likelihood:

CauseFix
DNS does not resolve here yetWait for propagation, or correct the A record
DNS points at Cloudflare's proxyThe orange cloud intercepts the port 80 challenge, so HTTP-01 cannot complete. Use Cloudflare + Full instead
Port 80 or 443 blockedOpen both. Port 80 answers the ACME challenge, 443 serves the issued certificate
Let's Encrypt rate limitToo many recent attempts for that domain. Wait for the window to reset

Let's Encrypt behind Cloudflare via DNS-01 is not supported. See Edge security & TLS.

The site is unreachable after setting a hostname

Expected, if you are still browsing by IP. Once a hostname is configured the edge drops any request whose Host header or TLS SNI does not match, closing the connection with HTTP 444 and no response body. Browse to the hostname instead.

Cloudflare returns 521 or 522

The origin lock is refusing the connection, or the origin is not listening where Cloudflare expects.

  • Flexible requires the origin to serve HTTP on port 80.
  • Full requires HTTPS on port 443 with a certificate present, even a self-signed one.

A mismatch between your Cloudflare SSL/TLS dashboard setting and the mode chosen here is the usual cause. Also confirm a host firewall has not blocked a Cloudflare range that was added recently.

Image pulls fail

unauthorized or denied from the registry

The org key in the pull path is wrong or revoked. The path segment is your VEIL_KEY lowercased:

terminal
docker pull registry.veilbrowser.net/<org-key-lower>/local-api:latest

install.sh derives this as VEIL_ORG_KEY_LOWER in .env. If you rotated the key, update both VEIL_KEY and VEIL_ORG_KEY_LOWER; changing only one leaves the pull working while the runtime fails, or the reverse.

No docker login is needed — the gate authenticates the private upstream for you. If you find yourself needing credentials, you are pointed at the upstream directly rather than through the gate.

manifest unknown

The tag does not exist for your channel. Check RELEASE_CHANNEL and prefer a :X.Y tag over a channel alias when you need reproducibility.

Browsers crash immediately

Almost always the Chromium sandbox or shared memory:

  • The worker container must run privileged, or have SYS_ADMIN with the supplied seccomp profile.
  • /dev/shm needs at least 1 GB. The Docker default of 64 MB causes renderer crashes under any real page load.
  • /tmp must be writable, and large enough for concurrent profile directories.

The single-host compose template sets all three; a hand-rolled docker run usually does not.

Config changes have no effect

The load-balancer configuration is derived state. Editing a cluster or host record updates the database, but the live edge keeps its previous configuration until you Render & Reload. Preview the diff first — if validation fails, the live config is left untouched and the operation records the error. See Operating the cluster.

Next steps

Was this page helpful?

On this page