kitesurf is a free, open source data extraction & web scraping project written in TypeScript and released under Apache-2.0. It has 1,328 GitHub stars, 59 forks and 35 open issues, and was last pushed 21 hours ago. On this registry it ranks #109 of 155 tracked projects in Data Extraction & Web Scraping, with 5 head-to-head comparisons available. It gained 152 stars over the last 2 tracked days.

What is kitesurf?

Kitesurf is an experimental browser engine, written in TypeScript and released under the Apache-2.0 licence, that parses HTML, applies CSS, runs page JavaScript and renders screenshots on Cloudflare Workers without Chrome or a browser container, and it is aimed at developers building agents and browser-automation tooling who want to drive a page runtime over the Chrome DevTools Protocol.

What it is

Kitesurf is a browser engine built to run inside Cloudflare Workers. It does the core work of a page load itself — parsing HTML, applying CSS, executing page JavaScript and producing screenshots — rather than delegating to an installed Chrome binary or a browser container. It speaks the Chrome DevTools Protocol (CDP), so CDP clients can address it as though it were a browser. The project is written in TypeScript and published under Apache-2.0, it belongs to the Cloudflare Workers ecosystem, and local development is handled by wrangler dev.

The concrete problem it solves is running and inspecting web pages in a worker-based, serverless environment where a browser is awkward to ship. Instead of standing up and maintaining a browser container alongside the rest of an automation stack, a developer points an existing CDP client — Puppeteer is the example the README gives — at the engine's WebSocket endpoint and receives rendered pages and screenshots. What would otherwise be a separate containerised Chrome install is replaced by a worker-hosted engine, and on Cloudflare it uses Dynamic Workers to create isolated workers for pages.

Key capabilities

  • Parses HTML, applies CSS, runs page JavaScript and renders screenshots without Chrome or a browser container.
  • Speaks the Chrome DevTools Protocol, so CDP clients such as Puppeteer can control it.
  • Accepts Puppeteer connections through puppeteer-core using the browser WebSocket endpoint at ws://127.0.0.1:8787/devtools/browser.
  • Publishes HTTP discovery at the /json/version endpoint and a health check at /health.
  • Serves Chrome DevTools for an inspected page through URLs such as ?url=https://example.com, or ?inspect=1 for a blank page, using either a bundled frontend on the engine's own origin or the frontend Google hosts at chrome-devtools-frontend.appspot.com; it also works with chrome://inspect once 127.0.0.1:8787 is added as a network target.
  • Creates isolated workers per page using Cloudflare Dynamic Workers.
  • Builds the WebAssembly modules and the page and renderer bundles with Bazel: pnpm run dev runs that build and then starts wrangler dev, with the first build compiling the Rust core and its vendored crates and later builds reusing Bazel's cache.

Who uses it and how

  • Agent and browser-automation developers, the two topics attached to the repository, who need page rendering and screenshots inside a Worker rather than in a separate browser container.
  • Teams that already hold Puppeteer automation can reuse their CDP client code by connecting to the browser WebSocket endpoint instead of launching a browser.
  • Developers debugging page behaviour locally run pnpm run dev, which serves the engine on 127.0.0.1:8787, then open Chrome DevTools against it or register the address in chrome://inspect.
  • Deployments on Cloudflare require the Workers Paid plan, because the Dynamic Workers that provide isolated per-page workers are part of that plan.

Getting started

Clone the repository, run pnpm install --frozen-lockfile, and start the engine with pnpm run dev, which builds the WebAssembly modules with Bazel and launches wrangler dev on http://127.0.0.1:8787. The local toolchain expects Bazelisk installed as bazel, Node.js 22 or newer, the pinned pnpm 9.15.4, GNU tar on macOS, and Rust through rustup only when editing the vendored crate patches.

How it compares

The facts do not provide a list of paid products that Kitesurf replaces, and no directly comparable engine is named among them either; the only related tooling the README positions it against is CDP client software such as Puppeteer, which drives Kitesurf rather than competing with it. The division of roles is therefore complementary: Puppeteer supplies the automation API and Kitesurf supplies the engine answering it, with rendering happening in a Worker instead of a Chrome installation or container. On the evidence available, it stands alone in this registry as a Worker-hosted browser engine.

When to use it — and when not to

A self-hoster has to operate a Bazel build toolchain, Node.js 22 or newer, the pinned pnpm 9.15.4 and, for crate patches, Rust, and must hold a Cloudflare Workers Paid plan to run it on Cloudflare at all. Anyone wanting a drop-in replacement for a full Chrome install, or a runtime that does not depend on a specific paid cloud plan, should look elsewhere, since the project is described as experimental. A further limitation is evident in the DevTools setup: the Google-hosted frontend can connect only to 127.0.0.1 and therefore does not work with a deployed engine unless the bundled frontend is built.

project readme (upstream, from github) — read inline

Kitesurf logo

Kitesurf

Kitesurf is an experimental browser engine that runs on Cloudflare Workers. It parses HTML, applies CSS, runs page JavaScript, and renders screenshots without Chrome or a browser container.

Kitesurf speaks the Chrome DevTools Protocol (CDP), so you can use it with CDP clients such as Puppeteer.

[!IMPORTANT] Kitesurf requires Cloudflare Dynamic Workers to create isolated workers for pages. Dynamic Workers are part of the Workers Paid plan. Local development uses wrangler dev. A Cloudflare Workers Paid plan is required to run Kitesurf on Cloudflare.

Requirements

  • Bazelisk, installed as bazel. Bazel builds everything. It downloads the pinned Bazel, Rust toolchains, wasm-bindgen, and binaryen itself.
  • Node.js 22 or newer
  • pnpm 9.15.4, which the repository pins in package.json
  • On macOS, GNU tar (brew install gnu-tar)
  • Rust through rustup, only for editing the vendored crate patches. rust-toolchain.toml pins the same nightly that Bazel uses.

Install pnpm separately if it is not already available:

npm install --global pnpm@9.15.4

Build and run locally

Clone the repository and install its dependencies:

git clone https://github.com/cloudflare/kitesurf.git
cd kitesurf
pnpm install --frozen-lockfile

Build and start the local engine:

pnpm run dev

The engine listens on http://127.0.0.1:8787. Visit http://127.0.0.1:8787/health to confirm that it is running.

The root URL shows the landing page. It has a form to open a URL in Chrome DevTools, example pages, and the CDP endpoints for agents and other clients. See Use Chrome DevTools.

pnpm run dev builds the WebAssembly modules and the page and renderer bundles with Bazel, then starts wrangler dev. The first build compiles the Rust core and its vendored crates, which takes a while; later builds reuse Bazel's cache.

Set KITESURF_PORT when port 8787 is already in use:

KITESURF_PORT=8799 pnpm run dev

Connect with Puppeteer

Install puppeteer-core in the project that will control Kitesurf. Then connect to the browser WebSocket endpoint:

import puppeteer from "puppeteer-core";

const browser = await puppeteer.connect({
  browserWSEndpoint: "ws://127.0.0.1:8787/devtools/browser",
});

const page = await browser.newPage();
await page.goto("https://example.com/");
await page.screenshot({ path: "page.png" });

await browser.disconnect();

Use the port from KITESURF_PORT if you selected a different port. HTTP discovery is available at http://127.0.0.1:8787/json/version.

Use Chrome DevTools

With pnpm run dev, open http://127.0.0.1:8787/ in Chrome. Enter a URL to inspect, or select one of the example pages. DevTools opens in the tab on that page. To go to DevTools directly, open http://127.0.0.1:8787/?url=https://example.com, or http://127.0.0.1:8787/?inspect=1 for a blank page.

If you built the bundled frontend, pnpm run dev serves it from the engine's own origin. See Use the bundled DevTools frontend. Otherwise, the DevTools frontend comes from chrome-devtools-frontend.appspot.com, the build that Google hosts for Chrome. The hosted frontend has these limits:

  • The first time it connects, Chrome asks whether chrome-devtools-frontend.appspot.com can access apps on this device. Select Allow.
  • If you block this access, DevTools says that the WebSocket disconnected. To allow it again, click the site information icon to the left of the address. Then allow the access and reconnect DevTools.
  • The frontend can connect only to 127.0.0.1. Thus it does not work with a deployed engine.

You can also use chrome://inspect:

  1. Open chrome://inspect/#devices in Chrome. Type or paste it into the address bar because web pages cannot link to it.
  2. Select Discover network targets.
  3. Select Configure... and add 127.0.0.1:8787.
  4. Under Remote Target, select inspect next to kitesurf.
  5. Enter a page URL in the address bar above the screencast.

Use the port from KITESURF_PORT when you select a different local port. Chrome discovers targets with plain HTTP and WebSocket connections. The chrome://inspect workflow cannot reach an HTTPS engine. Use it with a local engine instead.

Kitesurf provides a CDP endpoint, not a full Chrome implementation. Some Chrome DevTools features do not work yet. We are working to add more support over time. Puppeteer and other CDP clients can connect to ws://127.0.0.1:8787/devtools/browser.

Use the bundled DevTools frontend

Kitesurf can serve the Chrome DevTools frontend from its own origin. Then DevTools works without the hosted frontend, its permission prompt and the 127.0.0.1 limit. A deployed engine always uses this copy. The frontend is the prebuilt build that Google publishes for Chromium. The default build does not include it.

Build the frontend

The --config=devtools Bazel option adds the frontend to the build. The first build with it downloads the frontend (about 250 MB). Bazel keeps the download in its cache for later builds.

To build the frontend once, run this command from the repository root:

bazel run --config=devtools //packages/engine:dev_build

To include the frontend in every local build, add the option to .bazelrc.user. Git ignores this file. Then pnpm run build and pnpm run dev include the frontend.

echo 'common --config=devtools' >> .bazelrc.user

The build copies the frontend to packages/engine/public/inspector/. Git ignores this directory. A build without the option removes it. To turn the frontend off, remove the line from .bazelrc.user.

Run the frontend locally
  1. Build the frontend as described above.
  2. Start the engine with pnpm run dev. The engine serves the bundled frontend when the build includes it, and the hosted frontend when it does not.
  3. Open http://127.0.0.1:8787/ in Chrome. Enter a URL to inspect, or select one of the example pages. DevTools opens in the tab on that page.

To start on a page, open http://127.0.0.1:8787/?url=https://example.com.

Rebuild the frontend

Use the smallest step that fixes the problem:

  • To copy the frontend files again, delete the copy and build again:

    rm -rf packages/engine/public/inspector
    bazel run --config=devtools //packages/engine:dev_build
    
  • To extract the archive again, for example after you change bazel/devtools_frontend.BUILD, fetch it again and then build:

    bazel fetch --force --repo=@devtools_frontend --config=devtools
    bazel run --config=devtools //packages/engine:dev_build
    

    Bazel extracts the archive from its cache. To download the archive again from Google, add --repository_cache= to the bazel fetch command.

Do not use bazel clean --expunge for this. It also removes the WebAssembly build and the rest of the Bazel cache.

Update the frontend version

Two pins select the frontend version:

  • DEVTOOLS_FRONTEND_REV and the values after it in MODULE.bazel select the bundled frontend. The comment above them tells you how to select a new snapshot.
  • KITESURF_INSPECTOR_FALLBACK_URL in the local environment of packages/engine/wrangler.jsonc selects the hosted frontend. Use the Chrome commit that https://chromiumdash.appspot.com/fetch_version?version= returns in hashes.chromium.

Keep the two pins on the same Chrome version. After you change MODULE.bazel, the next build downloads the new archive.

Build and test

The pnpm scripts are front ends to Bazel:

pnpm run build                    # bazel run //packages/engine:dev_build
pnpm run check                    # type checks, bundles, and repository checks
pnpm run test:unit                # engine vitest and Rust host tests
pnpm run test:e2e <file-substring>
pnpm run format:check             # oxfmt; pnpm run format rewrites the same files
pnpm run format:rust              # format Rust with Bazel's pinned rustfmt
pnpm run lint                     # oxlint, the custom lint rules, and Rust lint

The Rust formatter rewrites tracked and non-ignored untracked files, excluding paths in .oxfmtrc.json.

pnpm run build copies the Bazel-built WebAssembly, bundles, and RBI files into the source tree, where wrangler dev reads them. Other targets run directly, for example bazel test //tests/e2e:gate for the full e2e suite. Another Bazel workspace can depend on this repository as a module.

Technology stack

Kitesurf combines several open-source Rust projects:

  • Blitz provides HTML parsing, the DOM, layout, and painting. It is licensed under MIT OR Apache-2.0.
  • Stylo provides CSS parsing, cascading, and computed styles. It is licensed under MPL-2.0.
  • Boa runs page JavaScript inside WebAssembly. It is licensed under Unlicense OR MIT.
  • Linebender libraries provide text layout and 2D graphics, including Parley, Vello, Kurbo, and Peniko. These libraries are licensed under Apache-2.0 OR MIT.
  • Krilla generates PDF output. It is licensed under MIT OR Apache-2.0.

Kitesurf patches its Rust dependencies. The patch series are in scripts/patches/, and Bazel applies them itself.

Cloudflare deployment

[!WARNING] Do not deploy Kitesurf without authentication in front of it. Use Cloudflare Access (Cloudflare Zero Trust) or another authentication layer. The engine has no authentication of its own. A person who has the URL can:

  • Open DevTools and browse the web through your engine.
  • Connect Puppeteer and other CDP clients to it.
  • Send outbound requests that come from your Cloudflare account.
  • Use CPU time and Dynamic Workers that your account pays for.

The workers.dev URL is public by default. Its name follows a predictable pattern: kitesurf..workers.dev.

To deploy the engine as a Worker on your Cloudflare account:

  1. Deploy the engine. Run the command from the repository root. In a package directory, use pnpm -w run deploy.

    pnpm run deploy
    
  2. Protect the Worker URL with Cloudflare Access immediately. Follow Manage access to workers.dev. For a custom domain or route, protect it with a self-hosted Access application.

  3. Open the Worker URL in Chrome. Access asks you to sign in. Then the landing page opens. Enter a URL to open it in DevTools.

Between steps 1 and 2, the engine is public. To prevent this gap, turn on Access for all Workers in the account before you deploy. The workers.dev page in step 2 tells you how.

The command builds the engine with Bazel and uploads it with Wrangler. Wrangler uses the account of wrangler login, or CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN. The command sends other arguments to wrangler deploy, for example --name . Without --name, the Worker name is kitesurf.

To build the deploy without an upload, run pnpm run deploy --dry-run.

To upload a version without sending traffic to it, run pnpm run deploy:preview. The command runs wrangler versions upload. If the Worker has Version URLs turned on, Wrangler prints the URL of the new version. Version URLs are public too. Protect them with Access as well. Follow Manage access to Version URLs.

The deploy always builds with --config=devtools, so it includes the Chrome DevTools frontend. You do not need .bazelrc.user for it. The first deploy downloads the frontend, as the first local build does. The deploy sets KITESURF_INSPECTOR_BASE_URL to /inspector/inspector.html. To use a different frontend, add --var KITESURF_INSPECTOR_BASE_URL: to the command.

DevTools loads from the Worker's own origin, so its WebSocket sends the Access cookie. A DevTools frontend on another origin cannot connect through Access.

Connect a CDP client through Access

Access blocks a CDP client that has no browser session. To connect Puppeteer:

  1. Create an Access service token.

  2. Add a policy with the Service Auth action that allows this token.

  3. Send the token headers with the WebSocket connection:

    const browser = await puppeteer.connect({
      browserWSEndpoint: "wss:///devtools/browser",
      headers: {
        "CF-Access-Client-Id": process.env.CF_ACCESS_CLIENT_ID,
        "CF-Access-Client-Secret": process.env.CF_ACCESS_CLIENT_SECRET,
      },
    });
    

Keep the service token secret. The token gives the same access as a signed-in user.

The engine configuration includes a Worker Loader binding. Kitesurf uses this binding to start a separate worker for each page and renderer. Cloudflare Dynamic Workers provide this capability, and they require the Workers Paid plan. See the Dynamic Workers pricing documentation before planning a Cloudflare deployment.

Packages

Package Description
engine Cloudflare Worker and CDP server
page-script Page DOM and JavaScript runtime
page-renderer Renderer for recorded page scenes
dom-shim DOM APIs inside the page worker
core Rust layout, paint, and JavaScript engine code
renderer-core Rust scene rasterizer for page rendering
scene-host-wasm Local scene rasterizer for the terminal client
kitesurf-cli Terminal client

Contributing

Read CONTRIBUTING.md before opening an issue or pull request. Report security problems as described in SECURITY.md.

License

Kitesurf is licensed under Apache 2.0. Bundled fonts, data, and dependencies keep their own licenses. See NOTICE.

Frequently asked questions

Is kitesurf free to use?

kitesurf is open source under the Apache-2.0 licence. There is no licence fee and no seat count — you can self-host it or, where the project offers one, pay a vendor for a managed version instead.

What does kitesurf do?

Kitesurf is an experimental browser engine that runs on Cloudflare Workers.

What is kitesurf written in?

kitesurf is primarily written in TypeScript. Its source is publicly available at https://github.com/cloudflare/kitesurf, and it has 1,328 GitHub stars.