Serverless Jupyter Notebooks at Meta

How Meta's Bento team used Pyodide and WebAssembly to run Python notebooks entirely in the browser, eliminating compute provisioning for lightweight workloads without changing the notebook interface.

Serverless Jupyter Notebooks at Meta

Most developer notebooks at Meta are not running ML training jobs, but every notebook still went through the same compute reservation process as the ones that are. The operational overhead exists to serve a workload profile that applies to only a fraction of usage — and for the majority of notebooks, all it produces is friction.

Source

Serverless Jupyter Notebooks at Meta

By: Steve Dini
Published: Jun 10 2024
Type: Engineering article

Saved for the kernel bridge abstraction design — the decision to present a browser-based execution environment through exactly the same interface as a server kernel, rather than building a parallel product for lite workloads. That pattern of hiding execution environment differences behind a clean protocol boundary is broadly applicable to any developer tool that spans multiple compute tiers.

Running Python in the Browser Without Provisioning Servers

Bento is Meta’s internal Jupyter notebooks platform. It serves a spectrum of workloads — quick SQL exploration, ad hoc data analysis, and large-scale ML training pipelines that need dedicated GPU compute. For years, all of them followed the same startup path: request remote compute, wait for provisioning, connect. For the engineering teams running ML pipelines, that’s acceptable overhead. For an analyst running a SELECT query and plotting the result, it’s a disproportionate cost for something that requires no GPU and touches no production write path.

The team’s definition of a “lite workload” is specific and useful: it only consumes data from upstream systems, has no side effects on underlying infrastructure, and stays within the Chrome tab memory limit. These constraints carve out a meaningful subset — data exploration, ad hoc ETL on small datasets, quick visualizations. The defining property is not workload size but workload shape: no writes to production, no C extensions that haven’t been compiled to Wasm, no memory requirements that exceed what a browser tab can hold.

The solution is Pyodide: a full Python distribution compiled to WebAssembly that runs entirely in the browser. WebAssembly provides near-native execution inside the browser sandbox, and Pyodide bundles Python’s standard library along with scientific computing packages — NumPy, Pandas, and others — into a Wasm binary the browser downloads and runs locally. For a lite workload, the user’s kernel is their tab. There is no SSH tunnel, no provisioning wait, no resource reservation.

Getting Pyodide to integrate cleanly into Bento required solving three separate problems.

The first is the kernel interface. Jupyter’s wire protocol assumes a kernel lives behind a socket. Bento’s existing notebook layer was built around this assumption. Rather than forking the notebook client to add a separate browser execution mode, the team built a kernel bridge — an abstraction layer that presents a Pyodide session using the same interface as any remote server kernel. In the notebook UI, this appears as a single toggle between “server-based kernel” and “serverless.” Nothing upstream of that bridge changes. The notebook itself doesn’t know whether Python is running in a WebWorker three meters away or on a provisioned host in a data center. This is the important design choice: the abstraction is at the protocol level, not the UI level. Switching kernels is not a mode change — it is literally a kernel swap.

The second problem is cell magics. Bento’s extension platform is built on cell magic commands — directives like %%sql, %%googlesheet, and %%graphql that invoke data connectors and inject results into the Python namespace. These magics are implemented in JavaScript in the main browser thread. The Pyodide kernel runs in a WebWorker, a separate thread with no direct DOM access. The fix is middleware that intercepts magic invocations before they reach the Python runtime, executes them in the JavaScript main thread, and injects the results back into the kernel as if they came from Python. For data reads, this is completely transparent. %%sql runs a warehouse query, and the result lands in the kernel’s namespace as a DataFrame — exactly as it would on a server kernel.

Write operations are the exception. Dataframe upload magic (%%dataframe) can’t follow the same intercept-and-inject pattern without granting browser JavaScript access to production write credentials. Instead, upload operations kick off a Tupperware job — Meta’s async compute tier — and surface the job status in the cell output. The write happens outside the browser sandbox, asynchronously, while the notebook stays interactive. This is the right boundary: anything that touches production write paths exits the browser model and becomes an explicit async operation.

The third problem is JavaScript’s single-threaded execution model. A CPU-heavy Python computation running on the main thread freezes the entire browser tab, including the notebook UI. Running Pyodide in a WebWorker moves computation off the main thread — results post back to the main thread on completion, and the UI stays responsive during long operations.

The roadmap makes this a foundation rather than a final state. The current version requires users to manually select serverless; the next step is auto-detection based on memory requirements, library imports, and data volumes. Once that’s reliable, new notebooks will default to serverless, and the system will automatically switch to a server kernel when it detects a workload that exceeds lite constraints. Further out, the plan is to route individual non-lite cells — those requiring C extensions or libraries not yet ported to Wasm — to remote execution while keeping the rest of the notebook in the browser.

The hard constraint is WebAssembly’s library coverage. Python packages with C extensions must be explicitly compiled to Wasm to run under Pyodide. Meta has many homegrown libraries that aren’t there yet. This doesn’t block the lite-workload case — standard data libraries are covered — but it caps how far the serverless path can reach until library coverage improves. The team is explicit about this: serverless notebooks are not a replacement for server kernels, they are a better default for the workloads that don’t need them.

My take

The kernel bridge abstraction is what makes this worth internalizing. A weaker design would have been two separate products: “Bento Lite” for browser workloads and “Bento Full” for server workloads. Users would have to choose up front, and any workload that started lite and grew would require migration. By hiding execution environment differences behind the existing kernel protocol, the team kept one product with two backends. The observable surface — the notebook, the cell magics, the output format — didn’t change. This is the general principle: when adding a new execution environment to an existing developer tool, design the abstraction at the protocol level and keep user-visible behavior identical. The implementation can swap; the interface should not.

Where this is useful

  • Designing developer tools that span multiple compute tiers, where the interface should abstract over the execution environment rather than exposing it
  • Evaluating WebAssembly as a runtime for interactive development environments where startup latency matters
  • Choosing between sync and async patterns for write operations from browser-constrained contexts — Tupperware for writes is an example of the right boundary
  • When NOT applicable: workloads requiring GPU compute, C extensions without Wasm ports, or operations with production side effects that can’t safely be proxied through an async tier

Apply this: When adding a new execution backend to an existing developer tool, define the abstraction boundary at the lowest-level protocol the tool already uses — kernel protocol, RPC interface, message bus — so the new backend can plug in without changing how users interact with the tool.

What I want to remember

Remember: Hide execution environment differences at the protocol level, not the UI level — a kernel swap should be invisible to the notebook, to cell magics, and to users, so the decision about where code runs can be made and changed automatically without requiring users to migrate.