# Lightpanda: driving a headless Zig browser over CDP, and the glibc catch

> Lightpanda is a headless browser written from scratch in Zig for AI agents and automation, driven by Puppeteer over a CDP server on port 9222. It installs with curl, brew, AUR or Docker, it fails on musl Linux and Android, and it is AGPL-3.0.

**lightpanda-io/browser** — Lightpanda: the headless browser designed for AI and automation. Start a CDP server Once the CDP server started, you can run a Puppeteer script by configuring the browserWSEndpoint.

- Repository: https://github.com/lightpanda-io/browser
- Website: https://lightpanda.io
- Stars: 35,571 · Forks: 1,688
- Language: Zig
- License: AGPL-3.0
- Published: 2026-08-08 · Updated: 2026-08-18 · Language: en
- Canonical page: https://hysenlabs.com/projects/lightpanda-io-browser

## The binary needs glibc, so Alpine and Termux fail at exec time

The Linux release binaries are linked against glibc, and the README is blunt about the consequence. On a musl distribution such as Alpine the dynamic loader is missing, and the binary dies at exec time with `cannot execute: required file not found`. Android and Termux reach the same error by a different route: there is no native Android build, and the Linux aarch64 binary needs `/lib/ld-linux-aarch64.so.1`, a path Bionic libc does not provide.

There are two ways out, and the fix is a base image or a source build rather than a flag. Use a glibc based image, either `FROM debian:bookworm-slim` or `FROM ubuntu:24.04`, or build from sources. The repository's own Dockerfile starts from `debian:stable-slim`, which is that decision already made for you. If your pipeline image is Alpine, or the target device is a phone, the price is a rebuild of the image or of the browser, and that is worth settling before anything else here.

## Four install routes, and every one of them tracks a nightly tag

Homebrew and the Arch User Repository both take the nightly channel:

```bash
brew install lightpanda-io/browser/lightpanda
```

```bash
yay -S lightpanda-nightly-bin
```

The manual route is a curl and a chmod:

```bash
curl -L -o lightpanda https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-x86_64-linux && \
chmod a+x ./lightpanda
```

Substitute the aarch64 or macos asset name for your platform; the nightly builds page carries x86_64 and aarch64 binaries for both Linux and MacOS. Check what landed before you point anything at it:

```bash
./lightpanda version
```

The fourth route is a container, and it is the one that sidesteps the glibc problem:

```bash
docker run -d --name lightpanda -p 127.0.0.1:9222:9222 lightpanda/browser:nightly
```

Official images exist for linux amd64 and arm64, and that command publishes the CDP server on port 9222 bound to 127.0.0.1. What all four routes share is the version question: none of them pins a release, because the tags are nightly, while the published releases are 0.4.1 on 2026-09-15, 0.4.0 on 2026-08-31 and 0.3.7 on 2026-08-16.

## fetch dumps a single URL, serve hands out a CDP endpoint

Two subcommands carry most of the work. `fetch` takes a URL and prints the result:

```bash
./lightpanda fetch --obey-robots --dump html --log-format pretty  --log-level info https://demo-browser.lightpanda.io/campfire-commerce/
```

`--dump markdown` converts straight to markdown, and `--dump png > page.png` or `--dump pdf > page.pdf` produce a text-only rendering of the page. Waiting is explicit rather than implicit: `--wait-until`, `--wait-ms`, `--wait-selector` and `--wait-script` adjust how long the dump waits, so a page that hydrates late is a setting for you to choose, not a sleep to sit through.

`serve` turns the same binary into a CDP server:

```bash
./lightpanda serve --obey-robots --log-format pretty  --log-level info --host 127.0.0.1 --port 9222
```

Both subcommands take `--obey-robots`, both take the same logging flags, and both put the host and port in the same place for a server that other machines can reach. The two modes differ in what comes out at the end, not in how careful they are about what they fetch.

## Puppeteer connects through browserWSEndpoint, and BiDi is one flag

Once the CDP server is up, a Puppeteer script points at it and stops caring what answers on the other end:

```js
import puppeteer from 'puppeteer-core';

// use browserWSEndpoint to pass the Lightpanda's CDP server address.
const browser = await puppeteer.connect({
  browserWSEndpoint: "ws://127.0.0.1:9222",
});
```

The example in the README then creates a browser context, opens a page, and navigates with `{waitUntil: "networkidle0"}` before evaluating a selector query in the page. A comment in that script says the rest of it stays the same, and that is the practical claim worth checking against your own code: the client speaks CDP to a server that is not Chrome, so your selectors and evaluate calls are not rewritten. Anything you rely on that only a Chromium build implements is where this substitution shows up first.

WebDriver BiDi is a switch, not a second binary. `--protocol webdriver` enables it, `--protocol webdriver --protocol cdp` starts both, and the serve line above gains those two arguments and nothing else.

## The 16x and 9x figures come from a crawler run, which bounds them

The headline says 16x lighter and 9x faster than Chromium. The table underneath names the conditions: 933 real web pages requested over the network on an AWS EC2 m5.large instance. Peak memory across 100 pages is 123MB against 2GB for headless Chrome, and execution time for 100 pages is 5s against 46s.

Read what that setup actually measures. It is a crawler pulling pages over a network, not a rendering benchmark, and the memory number is a peak across a hundred pages rather than a per tab figure. The comparison target is headless Chrome rather than every other engine, and the numbers are published by the project next to a benchmark details file kept in a separate demo repository, not in an independent harness. Nothing reported here covers frame rate, script speed, or how a long lived page behaves. One heavy interactive tab is a different workload from a thousand URLs, and these ratios are the wrong input for it.

## Agent mode exports a PandaScript that runs without a model

`lightpanda agent` drives the browser from a task written in plain English or in slash commands: navigate pages, click through flows, fill forms, extract structured data. The agent runs inside the same process as the browser, so each tool call is a direct operation instead of a round trip to a separate service. The output of a session is a PandaScript, which is vanilla JavaScript with a small set of native browser primitives built into Lightpanda. Run `/save` to export one, then replay it with `lightpanda run <script>.js`.

The scripts are deterministic and token-free, and that is the part that reaches production: prototype with a model, export the script, then run the JavaScript with no model at runtime. Providers named for the agent include Anthropic, OpenAI, Gemini, Google Vertex AI, Mistral, Hugging Face, the Vercel AI Gateway and OpenRouter, any OpenAI compatible endpoint through `OPENAI_BASE_URL`, and local models through Ollama or llama.cpp. `--no-llm` drops you into a REPL with no model at all. The trade is that the exported script repeats the recorded steps, and the README documents no way for it to re-plan while it runs.

## A source build means Zig plus a prebuilt V8 from a fork

Building from sources is a Zig project, and the file names say so: build.zig, build.zig.zon, a Makefile, and flake.nix with flake.lock for Nix users. The Makefile is where the cost of a build is written down. Compiling V8 from source takes more than ten minutes, so `make download-v8` fetches a matching prebuilt archive from the zig-v8-fork releases and leaves the caching to build.zig. The archive is named `libc_v8_$(V8_VERSION)_$(OS)_$(ARCH).a`, and the V8 and fork versions are read out of .github/actions/install/action.yml so a local build follows CI instead of drifting away from it.

Tests go through the same entry point with a filter, `make test F="server"`, and extra flags travel in ZIGFLAGS, for example `ZIGFLAGS=-Ddev_fast=false make test`. The Makefile also derives OS and ARCH from uname -ms and stops with an error on a kernel combination it does not recognise. The Dockerfile reaches the same dependency a different way, installing Rust, minisign and the Zig version named in build.zig.zon, then verifying the Zig tarball signature with minisign before unpacking it.

## Windows has no native binary, so WSL2 forwards port 9222 for you

There is no native Windows build. The route is WSL, following the Linux steps above, and one detail makes that arrangement usable: WSL forwards `localhost:9222` automatically, so Puppeteer or Playwright can run either inside WSL or on the Windows host and still reach the server. If WSL is not installed, run `wsl --install` from an administrator shell, restart, then open `wsl`.

The caveat is where the commands run. Every invocation, including the `serve` line that opens port 9222, has to happen under WSL, and a native Windows process reaching a WSL service crosses a port forward rather than a socket on the same stack. The repository publishes no measurement of what that costs, so a chatty client on a chatty network is a case to try before committing to it. A Docker container on the host is the other way to keep the two sides in one place.

## Conclusion

Adopt Lightpanda when the workload is many fetches or an agent loop rather than one heavy interactive tab, and when the host is glibc Linux, macOS, or WSL2 on Windows. Skip it on Alpine, on Android and Termux, and anywhere the answer depends on rendering fidelity that the crawler benchmark never measured. Before adopting, pin a version: every install route in the README tracks the nightly tag, the released versions are 0.4.1 from 2026-09-15, 0.4.0 from 2026-08-31 and 0.3.7 from 2026-08-16, and the last push to main was 2026-09-25. Read LICENSING.md next, because the AGPL-3.0 licence is the clause most likely to change how you integrate it.

## FAQ

### Does Lightpanda run on Alpine Linux or on Android?

Not from the release binaries. They are linked against glibc, so on musl distributions such as Alpine and on Android or Termux the binary fails with `cannot execute: required file not found`. Use a glibc base image such as `FROM debian:bookworm-slim` or `FROM ubuntu:24.04`, or build from sources.

### How do I connect Puppeteer to Lightpanda?

Start the CDP server with `serve`, for example on `--host 127.0.0.1 --port 9222`, and point the client at it with the `browserWSEndpoint` option set to `ws://127.0.0.1:9222`. The rest of the Puppeteer script stays the same, and a Docker run exposes the same port 9222.

### What is a PandaScript, and do I need an LLM in production?

It is the output of an agent session: vanilla JavaScript with a small set of native browser primitives built directly into Lightpanda. Run `/save` in the session to export one and replay it with `lightpanda run <script>.js`, and because the scripts are deterministic and token-free you can ship them with no model at runtime.

### Is there a native Lightpanda build for Windows?

No native Windows binary exists, so it is installed inside WSL following the Linux steps. WSL forwards `localhost:9222` automatically, which lets your automation client run either inside WSL or on the Windows host.

### What licence is Lightpanda released under?

The repository is licensed under AGPL-3.0 and carries a separate LICENSING.md alongside the LICENSE file, so the terms worth reading before you integrate it are in those two files rather than in the README.

## Sources

- [Official documentation](https://lightpanda.io)
- [Official README](https://github.com/lightpanda-io/browser#readme)
- [Project repository](https://github.com/lightpanda-io/browser)
- [Release notes](https://github.com/lightpanda-io/browser/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/lightpanda-io-browser
