# Moli: A Browser With the Lights Off – A Rust Headless Browser for AI Agents

> Moli is a Rust headless browser that does not wrap Chromium: one process and 43 MiB in a local test, against Chromium's seven processes and 159 MiB. But its default wait does not wait for the page's JavaScript data, and the empty table comes back with exit code 0.

- Author: [Halit Yeşil](https://halityesil.com/en/about/)
- Published: 2026-10-22
- Language: English
- Canonical URL: https://halityesil.com/en/moli-rust-headless-browser/
- Turkish version: https://halityesil.com/moli-headless-browser-rust.md
- Category: Artificial Intelligence / AI
- Tags: AI agents, Chromium, headless browser, Moli, open source, PHP, Playwright, Rust, web scraping
- License: https://creativecommons.org/licenses/by-nc/4.0/
- Cite as: Halit Yeşil. “Moli: A Browser With the Lights Off – A Rust Headless Browser for AI Agents”. halityesil.com, 2026-10-22. https://halityesil.com/en/moli-rust-headless-browser/

---

In the [Crawl4AI review](https://halityesil.com/en/crawl4ai-llm-web-crawler-review/) we talked about feeding a page to a model without the bones. The thing that actually opened the page in that piece was a Chromium instance spun up by Playwright. We were waking a multi-process desktop browser to read the text of one page, and nobody found that odd.

This week's radar entry objects to exactly that waste. **Moli** is a headless browser for AI agents, written in Rust. The repository was created on 10 August 2026 and sits at 19,751 stars as of 20 October; on [findarepo's 19 October list](https://findarepo.com/categories/web-scraping/) it is the fastest riser in the scraping category that week. I downloaded it into my cloud sandbox, ran it against a local test page and measured it next to Chromium. The most interesting finding was not a number. It was the empty table I got on the first try.

## What is Moli?

Moli is a standalone browser kernel that does not wrap Chromium. By default it runs the DOM and JavaScript without doing layout or paint. Underneath sit V8, html5ever, Servo's style engine Stylo, and libcurl for networking. The same binary is both a CLI and a CDP, WebDriver Classic and WebDriver BiDi server.

Think of Moli as a house with the lights off. The water runs, the power is on, the doors open, every room is where it should be. The lights just are not on, because you do not need light to read. When you need a screenshot or coordinates to click, `--layout` flips the breaker; when the job is done, the lights go off again.

## Installing: don't pipe it into your shell

The README suggests a one-line `curl ... | sh`. It works, but handing a 2.5 KB script you have not read to your shell should not be a habit. The release page publishes a SHA-256 for every asset, so I downloaded the archive and checked it:

```bash
curl -fsSLO https://github.com/lexmount/moli/releases/download/v1.1.18/moli-x86_64-unknown-linux-gnu.tar.gz
sha256sum moli-x86_64-unknown-linux-gnu.tar.gz
# value on the release page:
# 59715fbdb2b016bb9136e1edb59a2a195c0f98459fd06739e17edc62da6a48b1
tar xzf moli-x86_64-unknown-linux-gnu.tar.gz
./moli-v1.1.18-x86_64-unknown-linux-gnu/moli --version   # moli 1.1.18
```

The archive is 45 MB and unpacks to a single binary plus licence files. No separate browser download, no ChromeDriver.

## Measured: one process, about forty megabytes

The test page is deliberately boring: a top menu, an ad box, a footer, and a three-row exchange-rate table that JavaScript fills from `/kurlar.json`. I sent both browsers to the same local page five times and took the medians:

| Tool and command | Time | Memory (PSS) | Processes | Table filled |
| --- | ---: | ---: | ---: | --- |
| Moli 1.1.18, `--wait-selector` | 46 ms | 42.8 MiB | 1 | Yes |
| Chromium headless shell 141, `--dump-dom` | 355 ms | 159.4 MiB | 7 | Yes |

Do not read more into these than is there: a two-core sandbox, a one-kilobyte page, memory sampled every 5 ms. What travels is the process count and the order of magnitude. The project's own measurement paints a similar picture at larger scale (median memory across 192 real URLs: Moli 73 MiB, Chrome 773 MiB), but that measurement is the project's own. In its own benchmark suite, Lexbench, Moli passes 81.88% of 1,308 tasks and Chrome 99.85%; roughly one task in five works in Chrome and not in Moli. That figure also comes from a 13 August report on version 0.1.1.

## Don't walk into a dark house without ringing the bell

What matters more than the measurement is what I saw on the first attempt:

```bash
moli fetch --dump markdown http://127.0.0.1:8765/
```

Exit code 0, 72 ms. The output has the heading, the menu, the ad. The table has only its header row, with "Yükleniyor..." ("Loading...") above it. The default wait mode, `done`, did not wait for the page's own `fetch()` call to return. No error, no warning; the empty table was delivered as a success.

The fix is to give it a condition that proves the data arrived:

```bash
moli fetch --dump markdown --strip-mode ui \
  --wait-selector "#kurlar tbody tr" \
  --fail http://127.0.0.1:8765/
```

That returned all three rows in 39 ms. `--wait networkidle` also worked but took 533 ms; `domstable` took 390 ms. A bonus: `--strip-mode ui` dropped the menu, the ad box and the footer, leaving the heading, the update line and the table. That option has no description at all in `--help`; you only find out by trying it.

The same silence has three siblings:

- **An error page is a "success" too:** on a URL that does not exist, the exit code is still 0 and the 404 page comes back converted to Markdown. With `--fail` the code becomes 22.
- **The timeout is in milliseconds:** I wrote `--timeout 20` and got "timed out after 20 ms". For `fetch` the unit is milliseconds, default 25000. Type seconds out of curl habit and no page will ever open.
- **The door is wide open:** the default User-Agent says "Windows NT 10.0, Chrome/145" even on Linux. robots.txt checking (`--obey-robots`) and the private-network block (`--block-private-networks`) are off by default. If your agent takes URLs from users, this tool is an SSRF door until the second one is on. `--user-agent-suffix` is there to identify yourself.

## When it helps, and when it doesn't

**It helps** for pulling text and DOM from JavaScript-rendered pages in bulk, for giving agents a low-memory browser, and for keeping hundreds of sessions on one machine in evaluation and RL environments. `moli serve` listens on 127.0.0.1:9222 by default and Playwright attaches directly with `connectOverCDP`.

**It doesn't** for pixel parity, Canvas/WebGL or media playback; the README explicitly puts these out of scope. Lexmount's own cloud page also recommends Chromium when you need persistent contexts, extensions, downloads or high-fidelity visuals. And if you are reading one page of static HTML, curl and a parser are enough; you do not need a browser at all.

## Maintenance, and who does the work

The licence is Apache-2.0 or MIT, your choice. The first release, v0.1.0, landed on 11 August 2026; the latest, v1.1.18, on 17 October 2026. Thirty releases in ten weeks. 32 open issues, 372 open pull requests. There is no `SECURITY.md` and no published security advisory; at this age it is hard to tell whether that is good news or whether nobody has looked yet.

Moli is developed by [Lexmount](https://github.com/lexmount). The company's commercial product, Lexmount Browser, is a managed cloud browser service built around Moli; the README states the open source browser is fully usable without it. The commit history overwhelmingly belongs to one person: [Donough Liu (ldm0)](https://github.com/ldm0), with 1,965 contributions, whose profile lists LexMount and Beijing. The next contributor has 31. That is where the speed comes from, and where the risk comes from.

## How it compares

| Tool | Licence | Language | Stars | Latest release | Best for |
| --- | --- | --- | ---: | --- | --- |
| Moli | Apache-2.0 / MIT | Rust | 19.8k | v1.1.18 (17 Oct 2026) | Text and DOM from JS pages on little memory |
| Playwright + Chromium | Apache-2.0 | TypeScript | 98.1k | v1.64.1 (14 Oct 2026) | Visual fidelity, sessions, full compatibility |
| Lightpanda | AGPL-3.0 | Zig | 36.8k | 1.0.1 (15 Oct 2026) | Lighter still; lower success rate in Moli's own test |
| Crawl4AI | Apache-2.0 | Python | 85.9k | v0.9.5 (14 Oct 2026) | Not a browser: a cleaning crawler on top of Chromium |

Stars and releases taken from GitHub on 20 October 2026. Playwright is maintained by Microsoft, Lightpanda by the Lightpanda team, Crawl4AI by UncleCode. If you plan to embed Lightpanda inside a service, its AGPL licence is the line to read before anything else.

## Three ways in from the agent side

You can wire Moli into an agent three ways, and none of them locks you to one vendor. First, plain commands: terminal agents such as Claude Code, Codex or OpenCode call `moli fetch` as a shell command. Second, the repository's `skills/` folder: three skills in `SKILL.md` format, and the description of `moli-webfetch` names Codex directly; Claude Code reads the same format as a skill. Third, CDP: Playwright, Puppeteer or any automation layer that speaks CDP attaches to `moli serve`.

## Calling it from plain PHP

My use case is pulling a JavaScript-rendered price table from cron and writing it to the database. This framework-free PHP function calls Moli through `proc_open` and closes all four traps above:

```php
<?php
declare(strict_types=1);

function moliSayfa(
    string $url,
    string $bekleSecici,
    string $moli = 'moli',
    bool $icAg = false            // true only for local testing
): string {
    $komut = [
        $moli, 'fetch',
        '--dump', 'markdown',
        '--strip-mode', 'ui',              // drop nav, aside, footer
        '--wait-selector', $bekleSecici,   // don't return before the data
        '--fail',                          // exit code 22 on 4xx/5xx
        '--user-agent-suffix', 'halityesil-bot/1.0',
        '--timeout', '20000',              // milliseconds!
        $url,
    ];
    if (!$icAg) {
        array_splice($komut, -1, 0, ['--block-private-networks']); // no private-network targets
    }

    $surec = proc_open($komut, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $borular);
    if (!is_resource($surec)) {
        throw new RuntimeException('Could not start Moli.');
    }

    $cikti = stream_get_contents($borular[1]);
    $hata  = stream_get_contents($borular[2]);
    fclose($borular[1]);
    fclose($borular[2]);
    $kod = proc_close($surec);

    if ($kod !== 0) {
        throw new RuntimeException("Moli exit code {$kod}: " . trim((string) $hata));
    }
    if (trim((string) $cikti) === '') {
        throw new RuntimeException('Moli returned empty output.');
    }

    return (string) $cikti;
}
```

It passed `php -l` and ran on PHP 8.3 against the local page: it returned the filled table, threw on a 404 with exit code 22, and refused to reach 127.0.0.1 with the private-network block on. Note that the prices in the Markdown are comma-decimal text such as "41,82"; the `floatval("49,350")` trap from [Urban Renewal #1](https://halityesil.com/en/urban-renewal-1-ai-refactoring-survey/) does not go away because you changed browsers.

## Touring the house with the lights off

Moli's pitch starts from the right place: most of what an agent does is about the page's structure, not its pixels. It delivers that structure in one process and about forty megabytes. But in a house with the lights off, an empty room and an occupied one look the same from the doorway. If you do not supply the wait condition, Moli hands you the empty room and says "here's the house", and confirms it with exit code 0.

Ring the bell (`--wait-selector`), read the sign on the door (`--fail`), and stay out of the neighbour's garden (`--block-private-networks`).

Stay technical, and read your exit codes.
