Back to articles
Back to articles
Engineering

How we built Browserbase Functions

Adam McQuilkin
Adam McQuilkinProject Lead, Core Engineer
Viv Nepenthe
Viv NepentheCore Engineer
February 10, 2026
9 min read
Share

When Browserbase was founded, we were solving a very specific problem. Browsers are hard to run in production. They’re stateful, long-lived, sensitive to environment differences, and notoriously painful to operate at scale. Hosting Chromium reliably is real infrastructure work, and most teams don’t want to do it themselves.

So we built a platform for hosted browser sessions. You create a session, connect over CDP, run Stagehand or Playwright, and Browserbase handles the rest.

That worked, but over time, we noticed a pattern kept repeating across almost every serious customer.

They still had to run the code somewhere.

Browserbase hosted the browser, but the logic that controlled it lived in a separate service. Teams built worker fleets, cron systems, queue consumers, or workflow engines just to execute Playwright scripts. That runner infrastructure was often harder to maintain than the browser itself.

Worse, it introduced latency and fragility in a very tangible way. The browser might be running in us-west-2 while your runner is sitting in us-east-1, or worse, in a different cloud entirely. Every CDP command and every Playwright/Stagehand action now has to cross that physical distance. In real automations, that boundary gets crossed hundreds of times per run, and the round trip time compounds into seconds of dead time and more surface area for flaky failures.

Browserbase Functions came out of that realization. If we already run the browser and understand its lifecycle deeply, then hosting the code that drives it isn’t a separate concern. It’s the same problem.

Copy link
Why we didn’t build “just another serverless platform”

We didn’t set out to build a generic functions-as-a-service product. Let’s be honest, there are plenty of those already, and most of them are optimized for short-lived HTTP handlers or stateless compute.

Browser automation is different.

Browser code is stateful during execution, it has tight feedback loops, it often runs longer than a typical request timeout, and it needs direct access to a live browser session. Designing Functions around those constraints shaped almost every engineering decision.

The goal was to make scaling browser automation feel like a single system instead of multiple loosely coupled ones.

Copy link
The core abstraction: functions, deployments, invocations

Early on, we ran into a seemingly simple question. What happens when you update a function?

If users invoke a function by name and you overwrite the code every time they deploy, then every deploy becomes a production deploy. That’s fine for a toy script, but it breaks down fast once anything depends on it. Real automations are flaky by nature, and “safe” tends to mean being able to change code, test it, roll it forward, and step back without accidentally changing what production is doing.

Forcing users to version names manually is just as painful. If you have to rename a function to deploy a new version, then you also have to update every caller, every job, every workflow, and every integration that points at it. The function name stops being an interface and starts being a deployment mechanism.

The solution was to separate identity from executable code.

A function is a stable logical resource. A deployment is an immutable version of that function’s code. An invocation is a single execution tied to a specific deployment.

When you create a new deployment, nothing about production traffic changes. You can invoke the new deployment directly to test it. Only when you explicitly promote it does the function’s production endpoint begin routing to that version. This gives users safe iteration without forcing them to rewire their systems every time they change code.

Copy link
Defining functions as infrastructure in code

Once versioning was solved, the next question was how developers should define functions in the first place.

Most serverless platforms rely on configuration files, directory conventions, or framework-specific entrypoints. We intentionally avoided that. Browser automation code tends to intertwine logic and runtime configuration. Splitting those concerns across files almost always creates drift.

Instead, Functions uses an infrastructure-as-code model where the definition lives alongside the handler itself.

import { defineFn } from "@browserbasehq/sdk-functions"; import { chromium } from "playwright-core"; // This is your first Browserbase function! // You can run it locally with: bb dev index.ts // Once ready, publish it with: bb publish index.ts type HNSubmission = { title: string | null; url: string | null; rank: number; }; defineFn("my-function", async (context) => { const { session } = context; console.log("Connecting to browser session:", session.id); // Connect to the browser instance const browser = await chromium.connectOverCDP(session.connectUrl); const browserContext = browser.contexts()[0]!; const page = browserContext.pages()[0]!; // Navigate to Hacker News console.log("Navigating to Hacker News..."); await page.goto("https://news.ycombinator.com"); // Wait for the content to load await page.waitForSelector(".athing", { timeout: 30000 }); // Extract the first three submission titles const titles = await page.evaluate(() => { const results: HNSubmission[] = []; document.querySelectorAll(".athing").forEach((submission, idx) => { if (idx >= 3) return; // only return 3 const titleElement = submission.querySelector(".titleline > a"); if (titleElement) { results.push({ title: titleElement.textContent ?? null, url: titleElement.getAttribute("href"), rank: idx + 1, }); } }); return results; }); console.log(`Successfully extracted ${titles.length} titles`); // Return the results return { message: "Successfully fetched top Hacker News stories", timestamp: new Date().toISOString(), results: titles, }; });

This approach has a few important consequences. Your code is the source of truth. You can import helpers, share logic, and structure your project like a normal application. There’s no separate manifest to keep in sync.

Under the hood, this requires a deterministic way to discover which functions exist.

Functions solves this by requiring an explicit entrypoint. During development and deployment, that entrypoint file is executed. Every call to defineFn registers itself in an internal function registry. That registry is then used to generate a manifest describing which functions exist, their configuration, and how they should be invoked.

If your code runs, your functions are discovered. There’s no static analysis step, no AST parsing, and no parallel configuration system. The execution model itself is the source of truth.

Copy link
Local development with real browser sessions

One of the biggest sources of pain in browser automation is environment drift. Code works locally, then behaves differently in production. We wanted to avoid that entirely.

When you run Functions locally, we don’t spin up a mock environment. The local dev server still creates real Browserbase sessions using your project credentials. Your code connects to an actual browser running in the same infrastructure it will use after deployment.

bb functions dev

Invocations hit a local HTTP endpoint, but everything behind that endpoint behaves the same way it does in production, with the same browser, session configuration, and CDP connection model.

That choice dramatically shortens the feedback loop. When something breaks, it breaks where you can see it. When it works locally, it’s much more likely to work when deployed.

Copy link
Deploying without breaking production

Publishing a Functions project bundles the code, builds it, and creates new deployments for every function defined in the entrypoint.

The important part is what doesn’t happen. Promotion is an explicit step, so production traffic doesn't automatically switch to the new code.

This mirrors how mature deployment systems work. You build first, test, promote intentionally, and then if something goes wrong, rolling back is just a matter of promoting a previous deployment.

The system is designed to make accidental breakage difficult.

Copy link
What happens when you invoke a function

Invoking a function is asynchronous by design. Browser automations don’t fit neatly into request response lifecycles, and gaslighting ourselves into thinking otherwise leads to brittle systems.

When you invoke a function, Browserbase creates an invocation record and schedules it for execution. A runner provisions an isolated execution environment that already contains the deployed code bundle. A browser session is created and injected into the runtime.

The runtime selects the correct handler by function name and executes it with a context object that includes session metadata and the CDP connect URL. When the handler completes, the result is persisted and the invocation is marked as completed, errored, or timed out. Because every invocation is tied to a real browser session, debugging flows through the same tooling you already use with Browserbase. You can inspect the session, review logs, and correlate failures without guessing where the code ran.

Copy link
Intentionally chosen constraints

Functions are stateless between invocations, execution time is bounded, and dependencies must be publicly resolvable, but trust me, these constraints are not accidents.

They make the system predictable, reduce operational complexity, and they encourage functions to be designed as units of work rather than long-lived services.

For workloads that require persistent state or complex orchestration, Functions fits cleanly alongside existing workflow engines rather than trying to replace them.

Copy link
Why this matters

We’re collapsing an entire class of infrastructure into the platform that already understands browser automation best.

By hosting both the browser and the code that drives it, we reduce latency, simplify deployment, and make production automation easier to reason about. Teams can focus on what their automation does instead of worrying about how it stays alive.

This is the foundation for where Browserbase is going. It has never been easier to running code powering browser agents.

Can't wait to see what you build.

npx @browserbasehq/sdk-functions init