What Does the Browserbase API Let You Build?
The Browserbase API gives you a cloud Chrome session over REST, then lets Playwright attach to it through a CDP WebSocket. You authenticate with an API key and project ID, call client.sessions.create(), and pass the returned connect_url to chromium.connect_over_cdp(). This tutorial builds that flow step by step, adds retry handling for 429 responses, and ends with a complete runnable Python script.
📌 TL;DR Executive Summary
- Core Takeaway:
POST /v1/sessionsreturns aconnectUrl; your Playwright client attaches to that remote Chrome over CDP and drives it with the normal page API. - Key Risk/Challenge: A new session returns HTTP 429 when you cross your plan’s concurrent-browser limit or its per-minute session-creation cap, and every session carries a one-minute minimum bill.
- Recommended Solution: Wrap creation in a retry helper that honours
retry-after, size your worker pool with a semaphore, and release each session in afinallyblock.
Browserbase runs real headless Chrome for automation clients. You do not build a VM image, install Chromium, or keep a browser pool warm: you request a session, connect, and release. Session management is the foundation — Fetch, Search, the Agent API, Functions and Sandboxes sit around it.
Prerequisites Before Your First API Call
- Python 3.9 or newer for the Python path. The SDK ships sync and async clients powered by httpx and is generated with Stainless.
- Node.js for the JavaScript path. The Node SDK is
@browserbasehq/sdkand ships TypeScript definitions for all request parameters and response fields. - An API key and a project ID. The key authenticates the request; the project scopes sessions and billing. Omit
projectIdand the API infers it from the key. - Playwright installed locally. Puppeteer and Selenium are also listed as compatible clients; this tutorial uses Playwright because its CDP connection is direct.
- A page you own or are authorised to test. The examples use
example.comas a harmless smoke test.
Session creation, credentials and cleanup belong in different layers of a stack. If you are still deciding where, this guide to integrate cloud browser APIs into your workflow covers the architecture question first.
Step 1 — Install the SDK and Set Your API Key
Create a virtual environment so the SDK and Playwright stay pinned to this project, then install both packages. The Python SDK is published as a pre-release, so the install flag matters.
python -m venv .venv
source .venv/bin/activate
pip install --pre browserbase playwright
export BROWSERBASE_API_KEY="your_api_key"
export BROWSERBASE_PROJECT_ID="your_project_id"
On Windows PowerShell, set the same values with $env:BROWSERBASE_API_KEY="your_api_key" and $env:BROWSERBASE_PROJECT_ID="your_project_id". Out of the box the client retries connection errors and 408, 409, 429 and 5xx responses twice with short exponential backoff, and its default request timeout is one minute. The max_retries argument changes both behaviours.
Step 2 — Create a Session with the Browserbase API
Session creation is one SDK call that maps to POST /v1/sessions. The endpoint takes an optional projectId, a timeout in seconds, keepAlive, proxy settings, region and metadata. Timeout runs from a 60-second minimum to a 21,600-second maximum; leave it out and the project’s default timeout applies.
import os
from browserbase import Browserbase
client = Browserbase(api_key=os.environ["BROWSERBASE_API_KEY"])
session = client.sessions.create(
project_id=os.environ["BROWSERBASE_PROJECT_ID"],
timeout=300,
)
print(session.id)
print(session.connect_url)
The response carries connectUrl, wsUrl, a signingKey and the session ID. Playwright only needs connectUrl. Responses are Pydantic models, so session.to_dict() or session.to_json() gives you a loggable copy when you need the whole payload.
timeout to your expected job length plus a small buffer, not to 21,600. A long timeout does not make a slow job faster, but it does keep an idle browser alive longer if your cleanup path fails.
The full field list sits in the create-a-session reference. Read it before you add proxy or region values, because those decide where the browser exits and what the target page sees as its network.
Step 3 — Connect Playwright over CDP
Playwright attaches to the remote browser instead of launching a local one. A CDP connection can already carry a default browser context, so reuse browser.contexts[0] when it exists rather than opening a second context out of habit.
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(session.connect_url)
context = browser.contexts[0] if browser.contexts else browser.new_context()
page = context.new_page()
page.goto("https://example.com", wait_until="domcontentloaded")
print(page.title())
page.screenshot(path="browserbase-check.png")
browser.close()
This smoke test proves three things at once: the key can create a session, the connect URL is reachable from your machine, and Playwright can drive the page. A failure inside connect_over_cdp points at the URL, a firewall, or a session that already timed out. A failure at page.goto points at the target URL or the proxy you passed at creation time.
Step 4 — Release the Session Cleanly
Closing Playwright’s browser does not settle the bill. Release the session explicitly and put that call in a finally block, because a session has a one-minute minimum runtime and is billed for it even when it closes sooner.
client.sessions.update(session.id, status="REQUEST_RELEASE")
Use the release call for short jobs: a screenshot, a smoke test, one form submission. Setting keepAlive keeps a single session available for further connections across steps, so reserve it for work that genuinely needs continuity, such as a multi-page checkout you inspect in stages.
Step 5 — Handle 429 Rate Limits and the Per-Minute Cap
Two limits run side by side: concurrent browsers, and session creations per rolling 60 seconds. Cross the concurrency limit and the new session request returns HTTP 429 and is dropped. Rate-limited responses expose x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset and retry-after, so your code can wait exactly as long as the API asks.
import time
from browserbase import RateLimitError
def create_session_with_retry(client, project_id, attempts=5):
for attempt in range(attempts):
try:
return client.sessions.create(project_id=project_id, timeout=300)
except RateLimitError as exc:
if attempt == attempts - 1:
raise
retry_after = float(exc.response.headers.get("retry-after", "2"))
time.sleep(retry_after)
The SDK already retries twice by itself. That default absorbs a blip; it does not absorb a burst of parallel workers hitting the per-minute creation cap. Keep your own loop, read retry-after rather than guessing a sleep window, and let the final attempt raise so the failure reaches your logs instead of disappearing.
Header names and limit definitions are documented in Browserbase’s concurrency overview. Do not hardcode a reset window; the header is the contract.
Step 6 — Run Concurrent Sessions Without Exceeding Your Plan
Concurrency is a plan property, not a property of your URL list. Size the worker pool to the browser count your plan allows and let a queue handle the remainder.
| Plan | Monthly price | Included browser hours | Concurrent browsers | Session creations per minute |
|---|---|---|---|---|
| Free | $0 | 1 | 3 | 5 |
| Developer | $20 | 100, then $0.12/browser-hour | 25 | 25 |
| Startup | $99 | 500, then $0.10/browser-hour | 100 | 50 |
| Scale | Custom | Usage-based | 250+ | 150+ |
The plan price is not the bill. Overage, proxy traffic and short sessions all move the total, which is why it pays to read Browserbase pricing beyond the plan price before you schedule a large batch.
In async Python, use AsyncBrowserbase with a semaphore sized to the concurrent-browser limit. This example runs three workers, shares one Playwright instance, and releases each session in finally.
import asyncio
import os
from browserbase import AsyncBrowserbase
from playwright.async_api import async_playwright
API_KEY = os.environ["BROWSERBASE_API_KEY"]
PROJECT_ID = os.environ["BROWSERBASE_PROJECT_ID"]
CONCURRENCY = 3 # match your plan's concurrent-browser limit
async def check_page(playwright, client, sem, url):
async with sem:
session = await client.sessions.create(project_id=PROJECT_ID, timeout=300)
try:
browser = await playwright.chromium.connect_over_cdp(session.connect_url)
context = (
browser.contexts[0]
if browser.contexts
else await browser.new_context()
)
page = await context.new_page()
await page.goto(url, wait_until="domcontentloaded")
title = await page.title()
await browser.close()
return title
finally:
await client.sessions.update(session.id, status="REQUEST_RELEASE")
async def main():
client = AsyncBrowserbase(api_key=API_KEY)
sem = asyncio.Semaphore(CONCURRENCY)
urls = ["https://example.com", "https://example.org", "https://example.net"] * 3
async with async_playwright() as playwright:
results = await asyncio.gather(
*(check_page(playwright, client, sem, url) for url in urls)
)
for title in results:
print(title)
if __name__ == "__main__":
asyncio.run(main())
For heavier async throughput, install browserbase[aiohttp] and pass http_client=DefaultAioHttpClient() to AsyncBrowserbase. That swaps the HTTP transport underneath the SDK. It does not change your plan limits, so the semaphore stays exactly where it is.
CONCURRENCY from the plan table, never from len(urls). A semaphore of 25 on the Free plan does not create 25 browsers; it creates a queue of 429 responses you then have to wait out.
Step 7 — The Same Session Flow in Node.js
The Node SDK follows the same model: install two packages, create a client, request a session, connect over CDP, release. Its TypeScript definitions cover request parameters and response fields, so your editor flags a bad field before the request reaches the API.
npm install @browserbasehq/sdk playwright
import { Browserbase } from "@browserbasehq/sdk";
import { chromium } from "playwright";
const bb = new Browserbase({ apiKey: process.env.BROWSERBASE_API_KEY });
async function main() {
const session = await bb.sessions.create({
projectId: process.env.BROWSERBASE_PROJECT_ID,
timeout: 300,
});
try {
const browser = await chromium.connectOverCDP(session.connectUrl);
const context = browser.contexts()[0] ?? (await browser.newContext());
const page = await context.newPage();
await page.goto("https://example.com", { waitUntil: "domcontentloaded" });
console.log(await page.title());
await browser.close();
} finally {
await bb.sessions.update(session.id, { status: "REQUEST_RELEASE" });
}
}
main().catch(console.error);
Run it as an ES module, or place the file in a project with "type": "module" in package.json. The Node SDK maps HTTP failures to typed error classes, including RateLimitError, so the retry helper you wrote for Python ports across with little more than a keyword change. Keep the API key in the server environment and out of any browser bundle.
When to Use Agents, Stagehand, or Raw CDP
Raw CDP plus Playwright wins when you know the steps and want deterministic code: explicit navigation, explicit selectors, explicit assertions, and a failure that names the line it broke on. Choose Browserbase Agents when you would rather describe a goal in plain language and run it with one API call — they are generally available on every plan since May 2026, runs are asynchronous, and each returns data shaped by a result schema you define.
Stagehand is Browserbase’s open-source AI browser-automation framework, and you host the agent loop yourself, which buys control at the cost of owning the orchestration. Functions sit in a different layer: a hosted runtime that creates and cleans up sessions for you, now with Secrets and Webhooks so credentials are read at runtime from the function context instead of being hardcoded. That matters most when manual lifecycle code has become the source of leaked sessions. If you are weighing managed infrastructure against other providers, this comparison of Browserbase alternatives is a reasonable sanity check before you commit to an architecture.
Common Browserbase API Errors and Fixes
| Error | What it usually means | Fix |
|---|---|---|
400 BadRequestError |
A parameter is malformed or out of range, such as a timeout below 60 or above 21,600 seconds. | Check the project ID, timeout and proxy object against the session schema. |
401 AuthenticationError |
The API key is missing, revoked, or copied with stray whitespace. | Re-export BROWSERBASE_API_KEY and confirm the client reads the same environment. |
403 PermissionDeniedError |
The key is valid but cannot reach that project. | Use a key that belongs to the project, or correct the project ID. |
404 NotFoundError |
A session or resource ID does not exist, often because the session already timed out. | Create a fresh session; do not cache session IDs between runs. |
422 UnprocessableEntityError |
The JSON parses but a value breaks a business rule. | Read the message and change the field rather than retrying it unchanged. |
429 RateLimitError |
You hit the concurrent-browser limit or the creation cap. | Honour retry-after, lower concurrency, queue work behind a semaphore. |
5xx InternalServerError |
Browserbase had a server-side problem. | Retry with backoff; the SDK already retries selected 5xx responses twice. |
Two failures never arrive as error objects. If connect_over_cdp hangs, confirm you are passing connectUrl and not wsUrl. If the browser disconnects mid-job, check whether your session timeout was shorter than the work. The retry-and-release pattern also transfers to other vendors’ endpoints; see how to call a browserless API from Python to compare request shapes.
How Send.win Helps With Browserbase Api
Send.win is a browser profile manager for professionals and teams: every account gets its own isolated browser profile.
- Isolated profiles – separate cookies, storage and sessions per profile
- Fingerprint protection – each profile keeps its own consistent browser fingerprint, so profiles never share one browser identity
- Desktop app + cloud browser – native app for Windows, macOS and Linux, or run profiles in the cloud with no install
- Residential proxies included – on Pro and Team, with timezone and locale matched to the proxy
- Team sharing – share a signed-in profile with a teammate without sharing the password
Try the free cloud demo — no install — or download the desktop app. Paid plans start at $19/month, and your first card purchase is covered by a 7-day money-back guarantee (see pricing).
The Full Final Python Script
This file combines setup, the retry helper, the Playwright connection, a screenshot and the release path. Set the two environment variables, install the packages from Step 1, and run it. It uses a 300-second session timeout and five attempts — enough for a smoke test without holding a browser open longer than the job needs.
import os
import time
from browserbase import Browserbase, RateLimitError
from playwright.sync_api import sync_playwright
API_KEY = os.environ["BROWSERBASE_API_KEY"]
PROJECT_ID = os.environ["BROWSERBASE_PROJECT_ID"]
TARGET_URL = "https://example.com"
TIMEOUT_SECONDS = 300
MAX_ATTEMPTS = 5
def create_session(client: Browserbase):
for attempt in range(MAX_ATTEMPTS):
try:
return client.sessions.create(
project_id=PROJECT_ID,
timeout=TIMEOUT_SECONDS,
)
except RateLimitError as exc:
if attempt == MAX_ATTEMPTS - 1:
raise
retry_after = float(exc.response.headers.get("retry-after", "2"))
print(f"rate limited; retrying in {retry_after}s")
time.sleep(retry_after)
def main():
client = Browserbase(api_key=API_KEY)
session = create_session(client)
print(f"session id: {session.id}")
try:
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(session.connect_url)
context = (
browser.contexts[0] if browser.contexts else browser.new_context()
)
page = context.new_page()
page.goto(TARGET_URL, wait_until="domcontentloaded")
print(f"page title: {page.title()}")
page.screenshot(path="browserbase-check.png", full_page=True)
browser.close()
finally:
client.sessions.update(session.id, status="REQUEST_RELEASE")
print("session released")
if __name__ == "__main__":
main()
Run it with python browserbase_check.py. You should see a session ID, the page title, a release confirmation, and a PNG in the working directory. Repeated rate-limit messages mean your batch is too wide for the plan: check the table, confirm no earlier run left sessions open, and reduce parallel workers before you retry.
🏆 Send.win Verdict
Browserbase is the right layer when the work is code-driven: a job queue triggers the request and your own program drives the remote browser from there. Send.win solves a different problem. It is a browser profile manager for people doing manual account work — each profile keeps its own cookies, storage, sessions, fingerprint settings and proxy, and a shared profile opens for a paid teammate already signed in. Reach for it when your team needs isolated browser profiles in a desktop app or in the cloud browser, with a person at the keyboard doing the work.
See Send.win plans — Pro from $19/mo with 150 profiles and residential proxies, or try the free cloud demo first.
Frequently Asked Questions
How do I get a Browserbase API key and project ID?
Both come from your Browserbase account. The key authenticates SDK and REST requests, and the project scopes sessions and billing. Store each in an environment variable, and remember that if you omit projectId the API infers it from the key.
How does the Browserbase Python SDK connect to Playwright?
Create a session with client.sessions.create(), read session.connect_url, and pass it to chromium.connect_over_cdp(). Reuse browser.contexts[0] when the connection already carries a context. Close the browser and release the session when the job ends.
What does a Browserbase session cost per browser hour?
Usage is measured in browser hours with a one-minute minimum per session. Developer includes 100 hours, then $0.12 per browser-hour. Startup includes 500 hours, then $0.10 per browser-hour. Free includes 1 hour, and Scale is usage-based.
Is there a free tier for Browserbase?
Yes. The Free plan is $0 and includes 1 browser hour, 3 concurrent browsers, 5 session creations per minute, 3 Agent runs and 1,000 Search and Fetch calls, with Search capped at 2 requests per second per project. The one-minute minimum still applies to every session you open.
What are the Browserbase rate limits and concurrency limits?
Concurrency is 3 browsers on Free, 25 on Developer, 100 on Startup and 250+ on Scale. Session creation is capped at 5 per minute on Free, 25 on Developer, 50 on Startup and 150+ on Scale. Crossing either limit can produce an HTTP 429.
How do I fix a 429 RateLimitError from Browserbase?
Read retry-after from the error response and wait that long before creating another session. Reduce parallel workers and queue the excess behind a semaphore sized to your plan. The SDK retries twice by default, but your own helper should cover the per-minute creation cap.
What is the difference between Browserbase Agents and Stagehand?
Agents are managed browser agents you define from a prompt and run with one API call; runs are asynchronous and return data shaped by a result schema. Stagehand is Browserbase’s open-source browser-automation framework where you host the agent loop. Use Agents for the managed path and Stagehand when you want the control.
Can Browserbase run Puppeteer or Selenium instead of Playwright?
Yes. Browserbase lists Playwright, Puppeteer and Selenium as compatible clients. The session still comes from the same endpoint and returns the same connect URL; only the client library and its connection call change. This tutorial uses Playwright because its CDP connection is the most direct.