Give Your AI Agent a Browser: Self-Host Browserless
Here's a problem you hit about five minutes into building anything with an AI agent: the model can reason all day, but it can't actually open a web page. It can't log into a dashboard, wait for the JavaScript to render, screenshot the result, or scrape a table that only exists after three API calls fire on the client. The moment your agent needs to touch the live web, you need a real browser somewhere.
So you do the obvious thing. You npm install puppeteer, launch Chrome inside your app container, and it works on your laptop. Then you ship it, and Chrome starts crashing under load, fonts render as tofu boxes, zombie processes pile up, and your 512MB container OOMs the first time two requests come in at once. I've been there. It's a bad afternoon.
Browserless fixes this by pulling the browser out of your app entirely and running it as its own service. Your agent talks to it over the network, and you never think about Chrome internals again.
What Browserless actually is
Browserless is a container that runs headless Chromium and exposes it two ways. You can connect the standard automation libraries (Puppeteer, Playwright, Selenium) to it over a WebSocket, or you can skip the library entirely and hit plain REST endpoints for the common jobs: grab rendered HTML, take a screenshot, generate a PDF, or run a scrape. It's the same Chrome you'd run locally, just living in its own box with a queue, a concurrency limit, and a token in front of it.
That last part matters for agents. An agent doesn't want to manage a browser lifecycle. It wants to say "give me the rendered text of this URL" and get an answer. REST endpoints are perfect for that.
Getting it running
The fastest path is the managed one. Browserless is on Elestio starting at $11/month, which gets you the container, auto SSL, backups, and monitoring without touching a Dockerfile. You can deploy Browserless on Elestio and have an HTTPS endpoint in a couple of minutes.
If you'd rather run it yourself, here's a Docker Compose file that won't fall over. The one setting people forget is shm_size. Docker gives a container 64MB of shared memory by default, and Chrome eats that almost immediately, then crashes in ways that look like anything but a memory problem.
services:
browserless:
image: ghcr.io/browserless/chromium
restart: unless-stopped
ports:
- "3000:3000"
shm_size: "2gb"
environment:
- "TOKEN=change-this-to-a-long-random-string"
- "CONCURRENT=10"
- "TIMEOUT=60000"
- "QUEUED=20"
CONCURRENT caps how many browsers run at once, QUEUED caps how many requests wait in line before you start returning 429s, and TIMEOUT (in milliseconds) kills sessions that run too long. Set these based on your VM size. A 2GB box is comfortable with 5 to 10 concurrent sessions, not 50.
The REST way (best for agents)
This is where Browserless earns its keep. Your agent doesn't need a browser library at all. To pull the fully rendered text off a page, it just posts a URL:
curl -X POST \
"https://your-instance.elestio.app/content?token=YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "waitForTimeout": 2000}'
You get back the HTML after JavaScript has run, which is the whole point. Want a screenshot instead? Same idea, different endpoint:
curl -X POST \
"https://your-instance.elestio.app/screenshot?token=YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "options": {"fullPage": true, "type": "png"}}' \
--output shot.png
Wrap either of those in a function and you've handed your agent a tool. Here's the shape of it in Python, the kind of thing you'd register as a callable for an LLM:
import requests
BROWSERLESS = "https://your-instance.elestio.app"
TOKEN = "YOUR_TOKEN"
def fetch_rendered_page(url: str) -> str:
"""Return the fully rendered HTML of a page for the agent to read."""
resp = requests.post(
f"{BROWSERLESS}/content",
params={"token": TOKEN},
json={"url": url, "waitForTimeout": 2000},
timeout=30,
)
resp.raise_for_status()
return resp.text
Here are the endpoints you'll reach for most:
| Endpoint | What it does |
|---|---|
| /content | Returns fully rendered HTML after JavaScript runs |
| /screenshot | Captures a PNG or JPEG of the page |
| Renders the page to a PDF | |
| /scrape | Extracts elements by CSS selector as structured JSON |
| /function | Runs custom browser code you send in the request |
The library way (for real interaction)
When the agent needs to click, type, or step through a multi-page flow, connect Playwright or Puppeteer over the WebSocket instead. Nothing runs Chrome locally, you just point the library at your instance:
const { chromium } = require("playwright");
const browser = await chromium.connectOverCDP(
"wss://your-instance.elestio.app?token=YOUR_TOKEN"
);
const page = await browser.newPage();
await page.goto("https://example.com");
await page.click("text=Sign in");
const title = await page.title();
await browser.close();
Same code you'd write against a local browser. The only change is connectOverCDP instead of launch, which means you can develop locally and flip to the remote instance in production by swapping one line.
The part everyone gets wrong
A few things will bite you if you're not ready for them:
- Chrome crashes with no clear error. Nine times out of ten it's
shm_size. Set it to 2gb and the mystery crashes stop. - You start getting 429s. Your queue is full. Either raise
CONCURRENTandQUEUEDif the VM has headroom, or add retry-with-backoff in your agent. Don't just crank the numbers on a small box, you'll trade 429s for OOMs. - Sessions hang forever. An agent that opens a page and never closes it will pin a browser slot. Always
close()in a finally block, and keepTIMEOUTset as a backstop. - 401 Unauthorized. The token is required on every single request, REST and WebSocket alike. If you rotate it, rotate it everywhere.
- Memory creeps up over hours. Long-lived browsers leak. Restarting the container on a schedule is a legitimate, boring fix.
Why bother self-hosting it
The hosted browser-API vendors charge per session or per minute, and an agent that checks pages in a loop racks that up fast. Running your own on an $11/month Elestio instance means unlimited sessions at a flat cost, your scraped data never leaves your infrastructure, and you can put it behind your own firewall. For anything privacy-sensitive, that last point is the whole ballgame.
Give your agent a browser it can actually use, keep the bill flat, and stop debugging Chrome inside your app container. That's a good trade.
Thanks for reading ❤️ See you in the next one 👋