By the fastCRW team · Credit costs and capabilities verified 2026-05-18 · Verify independently before relying on any number.
Disclosure: We build fastCRW, so weight this accordingly. This guide is written to be useful even if you stay on Puppeteer or Playwright for part of your workload — and we say plainly, below, where the browser stays the right tool.
Why migrate Puppeteer/Playwright to a scraping API
If you maintain Node-based Puppeteer or Playwright scrapers, you already know the tax. A headless Chrome context is heavy — roughly 1–2 GB of RAM per live context once a real page loads its JavaScript, fonts, and images — so a fleet that does dozens of pages in parallel turns into a memory and orchestration problem. On top of that you carry the maintenance: selectors drift, sites change layout, headless-detection scripts trip, and your "scraper" slowly becomes a browser-babysitting service.
The uncomfortable truth is that most scrapes do not need a full browser session. They need the clean text or a few structured fields off a page that happens to render with JavaScript. For that case, you can migrate Puppeteer to a scraping API: hand the URL to a managed endpoint, let it run the renderer, and get back Markdown or JSON. You drop the ~1–2 GB-per-context browser fleet and replace it with one HTTP call. fastCRW's engine is a single static Rust binary — a ~8 MB image, one container (a structural fact from the open-core README, not a benchmark) — versus the multi-gigabyte browser stack you operate today.
This post maps the Puppeteer/Playwright API surface onto fastCRW's /v1/scrape, shows a before/after port, and is explicit about the three things the API does not replace so you migrate the right scrapes and keep the browser for the rest.
Mapping the API surface
Most of a Puppeteer or Playwright scraper is glue: launch a browser, navigate, wait for content, pull HTML or evaluate selectors, then clean the result. A scraping API collapses that glue into request parameters. Here is the function-by-function mapping.
| Puppeteer / Playwright | fastCRW equivalent |
|---|---|
browser.newPage() + page.goto(url) | One POST /v1/scrape with { url } |
page.content() + an HTML-to-text/Markdown step | formats: ["markdown"] (the default) returns clean Markdown |
page.$eval() / page.evaluate() to pull fields | formats: ["json"] + a jsonSchema for the fields |
waitForSelector() / waitUntil: "networkidle" | The chrome renderer executes JS; auto decides per page |
| Managing your own headless Chrome pool | The managed renderer; you never launch a browser |
A few notes that matter for accuracy. fastCRW returns Markdown by default from /v1/scrape, so the entire "get HTML, then run Turndown/Readability to strip nav and ads" stage of your scraper disappears. For fields, you describe what you want with a jsonSchema and the engine fills it using a managed LLM — LLM-based JSON extraction is a managed feature available on paid plans (the FREE plan has no LLM features). The renderer choice (auto, http, lightpanda, chrome) is the lever that replaces your waitForSelector logic: auto tries fast paths first and falls back chrome → lightpanda → http, while chrome forces a full JavaScript render for the pages that genuinely need it.
Porting a script, concretely
Here is a representative Playwright scraper that grabs a product title and price from a JS-rendered page:
const browser = await chromium.launch(); → const page = await browser.newPage(); → await page.goto(url, { waitUntil: "networkidle" }); → const title = await page.$eval("h1", el => el.textContent); → const price = await page.$eval(".price", el => el.textContent); → await browser.close();
That is a browser launch, a navigation, a network-idle wait, two brittle selectors, and teardown — plus whatever retry and memory handling you bolted on around it. The fastCRW equivalent is one request that asks for typed fields:
POST /v1/scrape with body { "url": "https://example.com/product/42", "formats": ["json"], "jsonSchema": { "type": "object", "properties": { "title": { "type": "string" }, "price": { "type": "number" } } } }
The response carries title and price as typed fields. No selectors to maintain — if the site moves the price into a different element, schema-guided extraction usually still finds it, where a CSS selector would silently break.
Keep your Node code: a base-URL swap
You do not need a new SDK. fastCRW exposes a Firecrawl-compatible REST API, so if you already use the Firecrawl SDK (or are willing to adopt it), the migration is a drop-in after a base-URL swap — point api_url at your fastCRW instance and existing client code keeps working. That keeps the diff small: you are replacing the browser-driving module, not rewriting your whole pipeline. (A short known list of field-name and error-envelope differences exists; validate those before cutover — see our Firecrawl migration guide.)
Choosing http vs chrome — and the credit cost
Renderer choice affects quality and speed, but not credit cost. Every renderer — http, lightpanda, chrome, and auto — costs 1 credit for a Markdown scrape. There is no chrome surcharge. A static page and a fully JS-rendered page cost the same 1 credit; you pick the renderer based on what the page needs, not to manage cost.
One important caveat: the typed-field extraction the porting example above uses — any request with formats: ["json"] + a jsonSchema — is the 1-credit scrape plus the LLM token cost, billed as usage-metered LLM credits, because it runs an LLM over the page. That extra cost is the same regardless of renderer, and it scales with page size and token usage rather than being a fixed multiple. So budget by output, not renderer: a plain Markdown scrape is a flat 1 credit, while structured extraction adds usage-metered LLM credits on top. If you only need clean text, stay on formats: ["markdown"] and keep the flat 1-credit cost. For the broader cost picture and current tier pricing, see /pricing.
What stays in Puppeteer/Playwright
An API does not replace a browser for every job, and pretending otherwise would cost you a bad migration. Three limits decide what stays:
- Clicks, form fills, and logins. fastCRW is stateless per request — there is no persistent session that survives between calls, so multi-step flows (log in, navigate a wizard, submit a form, then read the result) are exactly what a real browser is for. If your scraper drives an interaction, keep it in Puppeteer/Playwright.
- Screenshots. fastCRW has no screenshot output — a request for
formats: ["screenshot"]returns HTTP 422. If you rely on Puppeteer'spage.screenshot()for visual diffs, PDFs of rendered pages, or image capture, that workload stays in the browser. - Heavily protected sites. fastCRW has no built-in anti-bot / Fire-engine. The
chromerenderer handles normal JS rendering, but it is not a managed proxy-rotation and fingerprint-evasion layer. Sites with aggressive bot defenses may still need your hardened browser setup or a dedicated proxy service.
State these to yourself honestly before you migrate a scraper, not after. A read-only scrape of a content or product page is an excellent API candidate; a login-walled, screenshot-producing, anti-bot-fighting flow is not.
A hybrid migration strategy
The pragmatic move is not "rip out Puppeteer." It is to split your scrapers by what they actually do:
- Inventory by interaction. Tag each scraper as read-only (navigate + extract) or interactive (clicks, forms, logins, screenshots).
- Migrate the read-only set first. These are the majority for most teams, and they are the ones eating browser RAM for no reason. Move them to
/v1/scrapevia the Firecrawl-compatible base-URL swap. This is where you reclaim the ~1–2 GB-per-context fleet cost and shrink to a single ~8 MB binary or managed credits — the win that low-memory scraping is built around. - Keep the interactive set in the browser. Let Puppeteer/Playwright do what only a stateful browser can. Many teams find the surviving browser fleet shrinks to a fraction of its former size.
- Measure before you fully commit. Run both paths on identical URLs for a couple of weeks and compare content parity, p50/p90 latency, and cost. Extraction quality is downstream of getting clean content out of the page in the first place — on Firecrawl's public 819-URL labeled dataset, fastCRW recorded the highest truth-recall of the three tools tested at 63.74%, with p50 1914 ms beating Firecrawl's 2305 ms (
diagnose_3way.py, 2026-05-08). In fast mode, fastCRW's p90 is 4348 ms — the lowest of the three (Crawl4AI 4754 ms, Firecrawl 6937 ms) — because the chrome-stealth fallback that recovers pages the others miss is what earns both the recall lead and the strong fast-mode tail. See /benchmarks.
A hybrid migration gives you the footprint win on the bulk of your scrapes without forcing the genuinely interactive ones through an API that was never meant to drive a browser session.
Where Puppeteer/Playwright genuinely win
To keep this honest: for stateful, multi-step automation — logging in, filling and submitting forms, clicking through paginated UIs that only respond to real events, and capturing screenshots or PDFs — Puppeteer and Playwright are the right tool and a scraping API is not a substitute. They also give you fine-grained, per-step control (intercept requests, mock responses, throttle network) that a single stateless call cannot express. If that is the core of your scraper, migrating it to an API would be a downgrade. For a deeper feature-by-feature look, see Playwright vs Puppeteer vs CRW and Playwright as a scraper vs fastCRW.
Sources
- fastCRW canonical facts (credits, renderers, gaps, footprint): github.com/us/crw open-core README and
- Scrape benchmark of record —
diagnose_3way.py, Firecrawl public dataset, 819 labeled URLs, 2026-05-08 (truth-recall 63.74%, p50 1914 ms, p90 fast-mode 4348 ms) - Live pricing and credit costs: /pricing · benchmark detail: /benchmarks
Related: Playwright as a scraper vs fastCRW · Playwright vs Puppeteer vs CRW · Migrate Selenium to a scraping API · Low-memory scraping
