# lol-html: streaming HTML rewriter with CSS selector-based API in Rust

> lol-html is a Rust library for rewriting HTML on the fly with minimal buffering, using CSS selector-based handlers that fire as each element arrives in the stream. It backs the HTML rewriting feature in Cloudflare Workers and is available as a standalone crate on crates.io.

**cloudflare/lol-html** — Low output latency streaming HTML parser/rewriter with CSS selector-based API

- Repository: https://github.com/cloudflare/lol-html
- Website: https://crates.io/crates/lol-html
- Stars: 2,071 · Forks: 113
- Language: Rust
- License: BSD-3-Clause
- Published: 2026-10-11 · Updated: 2026-10-11 · Language: en
- Canonical page: https://hysenlabs.com/projects/cloudflare-lol-html

## Streaming HTML rewriting designed for large documents and limited memory

lol-html is designed to modify HTML on the fly with minimal buffering. Its stated goals are handling very large documents and operating in environments with limited memory resources. The name expands to Low Output Latency HTML, and that latency concern is central: output begins flowing before the input is fully consumed, rather than waiting for a full parse and then emitting a rewritten document.

It is the library behind the HTML rewriting functionality in Cloudflare Workers, where it handles arbitrary web content under edge-compute constraints. That origin explains the buffering discipline. A Workers script rewriting response bodies cannot assume the document fits in memory, and any architecture that buffers full HTML before writing output would block the response stream. Workers processing millions of requests cannot afford that cost.

As a standalone crate, lol-html is useful beyond the Workers context. Any Rust service that needs to transform HTML from incoming HTTP responses, extract data from large pages, or process HTML incrementally can use it independently of the Workers runtime. A blog post at blog.cloudflare.com/html-parsing-2/ covers the design rationale. Documentation is at docs.rs/lol_html. The crate is published under the package name lol_html at crates.io.

## CSS selectors drive element handlers that fire on each matching node in the stream

The API organizes around handlers tied to CSS selectors. A handler is a closure that receives each element matching a given selector and can read attributes, set attributes, insert content before or after the element, or remove it. Handlers register on a Settings object, and the rewriter fires them as matching elements arrive in the input stream rather than after the document finishes parsing.

The element! macro shown in the README example illustrates the pattern: a CSS selector string paired with a closure taking an element reference. Multiple handlers for different selectors can register on the same Settings object and process in the order that matching elements appear in the stream.

This architecture means handler logic must be stateless relative to the document as a whole, or carry its own state explicitly through closures. A handler can collect data from one element into a variable it closes over, but it cannot look back at a previously seen element or ahead to one not yet parsed. The streaming constraint is the API constraint, and the two are not separable.

## Add lol_html from crates.io and feed the rewriter in chunks

lol-html is published on crates.io under the package name lol_html at version 3.0.1. Add it as a dependency in your project's Cargo.toml, then import the three core items from the crate:

```rust
use lol_html::{element, HtmlRewriter, Settings};
```

The rewriter is constructed with a Settings object that holds one or more element handlers. Once constructed, input is fed in arbitrary-sized byte slices using write(), and the output handler receives transformed byte slices as they are produced. The README example shows feeding a single HTML fragment in three separate calls:

```rust
rewriter.write(b"<div><a href=")?;
rewriter.write(b"http://example.com>")?;
rewriter.write(b"</a></div>")?;
rewriter.end()?;
```

Each write call delivers a byte slice to the rewriter. The output closure attached at construction time receives transformed slices and can write them directly to a network buffer, a file, or any other sink. Calling end() flushes remaining state and finalizes output. In a real streaming context the input slices come from a network read loop rather than from fixed byte literals.

## The streaming model prevents full-document lookahead and DOM-based queries

The limitation that follows from the design is the absence of a DOM. A handler cannot ask how many elements the document contains, cannot reach a sibling node, and cannot read content that appears later in the stream. Operations like counting all table rows before inserting a header row, or finding the last occurrence of a selector, are not expressible in the streaming API.

That boundary is a real constraint for content-heavy transformation tasks. Scraping a document for structured data where the meaning of one element depends on another requires either a second pass over collected output, or a different parser that builds a full tree.

html5ever appears in lol-html's dev-dependencies (version 0.39) and is used for comparison testing in the repository. It builds a full parse tree in memory, which is the cost lol-html avoids. Both handle HTML5-compliant parsing; the choice between them is whether the transformation needs lookahead into the document structure. Choosing lol-html when the task is DOM traversal leads to a rewrite later.

## encoding_rs handles character encoding; the selectors crate provides CSS selector parsing

Two dependencies handle the parsing layers that lol-html depends on. encoding_rs at version 0.8.35 manages character encoding. HTML documents can arrive in many encodings, and handling them correctly at the byte level before any selector matching is necessary for a parser that works on arbitrary web content. Cloudflare maintains encoding_rs as a separate project, and lol-html pins it at 0.8.35 in the published crate.

CSS selector parsing comes from the selectors crate at version 0.37 and the cssparser crate at version 0.36. These are the same selector libraries used in the Servo and Gecko browser engines for CSS matching. Using them means the selector syntax and matching semantics are well-tested against the CSS specification rather than relying on a custom implementation built for this library alone.

Other dependencies in Cargo.toml include bitflags 2.11.1 for flag management, hashbrown 0.17.0 with foldhash for fast hash maps, memchr 2.8.0 for byte searching, and thiserror 2.0 for error types. These are all standard Rust infrastructure crates, not specialty dependencies.

## A C API ships in the repository; Lua, Go and Ruby bindings exist outside it

A C API for embedding lol-html in non-Rust projects lives under c-api/ in the repository. This is an official binding maintained by Cloudflare, covering the core rewriting interface for projects that cannot use the Rust crate directly.

Three unofficial bindings exist outside the repository: a Lua binding at github.com/jdesgats/lua-lolhtml, a Go binding at github.com/coolspring8/go-lolhtml, and a Ruby binding called selma at github.com/gjtorikian/selma. The README notes that the Go and Ruby bindings do not come from Cloudflare. A js-api/ directory is also present at the repository root, though the README bindings section does not describe it.

The repository also contains a benches/ directory for performance benchmarks, a fuzz/ directory for fuzz testing, and a tools/ directory. Two complete usage examples are under examples/: defer_scripts/ and mixed_content_rewriter/. The DEVELOPING.md file documents the contributor workflow for those who want to work on the library itself.

## v3.0.1 requires Rust 1.85 and edition 2024; licence is BSD-3-Clause

Version 3.0.1 was released on 2026-07-29, which is also the date of the last push to the main branch. Prior releases were v3.0.0 on 2026-06-02 and v2.9.0 on 2026-05-06. The move from v2.9.0 to v3.0.0 is a major version bump; CHANGELOG.md at the repository root is where to look for breaking changes before upgrading.

The Cargo.toml sets rust-version to 1.85 and edition to 2024. A project on an older Rust toolchain cannot use this version of the crate without a toolchain upgrade. That minimum is enforced by cargo at compile time, not at runtime.

The licence is BSD-3-Clause, recorded in both the Cargo.toml licence field and the LICENSE file at the repository root. The author listed in Cargo.toml is Ivan Nikulin at Cloudflare (inikulin@cloudflare.com). The crate categories are parser-implementations and web-programming. A v2.9.0 release two months before v3.0.0 suggests active development in the first half of 2026, and the last push aligning with the v3.0.1 release date confirms the release came directly from that commit.

## Conclusion

lol-html suits teams writing Rust services that need to rewrite or scrape HTML from large or unknown-size documents under memory constraints, and Cloudflare Workers developers who want to run the same library the platform uses internally. Skip it when your work requires a full DOM: the streaming design buffers minimally on purpose, so operations that need the whole document before writing output do not fit this model. Before adding it to a production codebase, check CHANGELOG.md for breaking changes between v2.9.0 and v3.0.0, since that major bump happened on 2026-06-02.

## FAQ

### What is lol-html?

lol-html stands for Low Output Latency HTML. It is a Rust streaming HTML rewriter and parser with a CSS selector-based API, designed for modifying HTML on the fly with minimal buffering. It backs the HTML rewriting functionality in Cloudflare Workers.

### How do I install lol-html in a Rust project?

lol-html is published on crates.io under the package name lol_html at version 3.0.1. Add it to your Cargo.toml dependencies and import HtmlRewriter, Settings and the element! macro. Documentation is at docs.rs/lol_html.

### What Rust version does lol-html require?

Version 3.0.1 sets rust-version to 1.85 in its Cargo.toml and uses Rust edition 2024. Projects on older toolchains need an upgrade before the crate will compile.

### Does lol-html have bindings for other languages?

An official C API lives under c-api/ in the repository. Unofficial bindings exist for Lua, Go and Ruby, all maintained outside the Cloudflare repository. A js-api/ directory is also in the repository.

### What is the difference between lol-html and html5ever?

lol-html is a streaming rewriter that processes HTML in chunks and fires element handlers as selectors match, with minimal buffering. html5ever builds a full parse tree in memory. lol-html suits large or streaming documents; html5ever suits tasks that require DOM access or lookahead.

## Sources

- [cloudflare/lol-html on GitHub](https://github.com/cloudflare/lol-html)
- [License: BSD-3-Clause](https://github.com/cloudflare/lol-html/blob/main/LICENSE)
- [Project website](https://crates.io/crates/lol-html)
- [README](https://github.com/cloudflare/lol-html/blob/main/README.md)
- [Releases](https://github.com/cloudflare/lol-html/releases)

---

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