David Lewin David Lewin
Proxies & Web Scraping

Scrape JavaScript-heavy stores without running browsers: ZenRows vs self-hosted Playwright

Disclosure: some links in this guide are affiliate links. If you sign up through them, I may earn a commission at no extra cost to you. How this site makes money.

Short answer

ZenRows renders the page in its own headless browser, routes it through rotating residential IPs and handles anti-bot challenges, then returns the finished HTML from a single GET request. With JavaScript rendering and premium proxies a page costs 25 credits, which works out to about $6.90 per 1,000 pages on the $69 Launch plan and about $2.75 per 1,000 on the $549 Scale plan. Running Playwright yourself makes the browser almost free, but on a protected store you still pay for residential proxy bandwidth and for the hours spent keeping stealth patches working: by our estimate about $190 to $460 a month for 10,000 protected pages, against $69 on ZenRows. Self-hosting wins on high-volume sites that don't block you.

A plain HTTP request to a modern store often gets you an empty shell: the prices, stock levels and reviews are loaded by JavaScript after the page arrives. Add a bot-protection layer such as Cloudflare or DataDome and you also get challenges, blocks and CAPTCHAs. The usual answer is a fleet of headless browsers behind residential proxies, with stealth patches to hide the automation. This guide looks at the alternative: hand the whole problem to a scraping API, ZenRows, that renders the page and deals with the blocking for you, and compares what each approach costs per 1,000 rendered pages.

How this guide was made

ZenRows and ScraperAPI prices and credit rules come from their pricing pages and documentation, checked in October 2026. The Python client below was tested against a mock of the ZenRows API that enforces a five-request concurrency limit and returns temporary errors, not against live stores. Self-hosting costs use DigitalOcean's list prices plus our own estimates for proxy bandwidth per page and maintenance hours. They're estimates, not measurements, and your targets may differ a lot.

1. What a scraping API does with your request

You send one HTTPS request with the target URL and a couple of flags. Everything a browser cluster would do happens on ZenRows' side, and you get the rendered HTML back.

One request through ZenRows
One request through ZenRowsYour code → Residential IP and rotation / Headless browser / Anti-bot handling → Clean HTML back → Your parserYour code: GET api.zenrows.com/v1/ with url, js_render=true, premium_proxy=trueYour codeGET api.zenrows.com/v1/ with url, js_render=true,premium_proxy=trueResidential IP and rotation: A new exit IP per request, by country if you askResidential IPand rotationA new exit IP perrequest, bycountry if you askHeadless browser: Runs the page's JavaScript and waits for it to loadHeadless browserRuns the page'sJavaScript andwaits for it toloadAnti-bot handling: Browser fingerprint, challenges and retriesAnti-bothandlingBrowserfingerprint,challenges andretriesClean HTML back: The fully rendered page. Failed requests aren't billed.Clean HTML backThe fully rendered page. Failed requests aren't billed.Your parser: CSS selectors, JSON-LD or an extraction API, on HTML that is already completeYour parserCSS selectors, JSON-LD or an extraction API, on HTML that isalready complete
One request in, rendered HTML out: browsers, proxies and anti-bot handling are ZenRows' problem. Hover or tab through the boxes for details.
curl -G https://api.zenrows.com/v1/ \
  --data-urlencode "apikey=$ZENROWS_API_KEY" \
  --data-urlencode "url=https://example-store.com/products/blue-mug" \
  --data "js_render=true" \
  --data "premium_proxy=true"
  • js_render=true runs the page in a headless browser. Without it, ZenRows makes a plain HTTP request, which is cheaper and enough for server-rendered pages.
  • premium_proxy=true sends the request through residential IPs. Use it for stores that block datacenter IPs.
  • wait_for takes a CSS selector and waits for it to appear, which helps when prices load late.

2. A Python client you can run today

Standard library only. It fetches a list of URLs with five in parallel, retries the errors that are worth retrying and saves each page's HTML.

#!/usr/bin/env python3
"""Fetch rendered HTML through ZenRows, several pages at a time. Python 3.9+, standard library only.

  export ZENROWS_API_KEY=...
  python3 zr_fetch.py urls.txt html/
"""
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
from concurrent.futures import ThreadPoolExecutor
from hashlib import sha1

API = os.environ.get('ZENROWS_API_URL', 'https://api.zenrows.com/v1/')
KEY = os.environ['ZENROWS_API_KEY']
WORKERS = 5  # keep at or below your plan's concurrency


def fetch(url, premium=True, retries=4):
    params = {'apikey': KEY, 'url': url, 'js_render': 'true'}
    if premium:
        params['premium_proxy'] = 'true'  # residential IPs: 25 credits per page instead of 5
    q = API + '?' + urllib.parse.urlencode(params)
    for attempt in range(retries + 1):
        try:
            with urllib.request.urlopen(q, timeout=180) as r:
                return r.read()
        except urllib.error.HTTPError as e:
            # 429: too many requests at once for your plan; 5xx: temporary. Others (bad URL, no credits) won't fix themselves.
            if e.code not in (429, 500, 502, 503, 504) or attempt == retries:
                print('%s: HTTP %d %s' % (url, e.code, e.read()[:200]), file=sys.stderr)
                return None
            time.sleep(2 ** attempt)


def save(url, outdir):
    html = fetch(url)
    if html is not None:
        with open(os.path.join(outdir, sha1(url.encode()).hexdigest()[:16] + '.html'), 'wb') as f:
            f.write(html)
    return html is not None


def main():
    urls = [line.strip() for line in open(sys.argv[1]) if line.strip()]
    os.makedirs(sys.argv[2], exist_ok=True)
    with ThreadPoolExecutor(WORKERS) as pool:
        ok = sum(pool.map(lambda u: save(u, sys.argv[2]), urls))
    print('%d of %d pages saved' % (ok, len(urls)), file=sys.stderr)


if __name__ == '__main__':
    main()
  • Concurrency: every ZenRows plan has a limit on simultaneous requests, from 5 on the free plan to 200 or more on Scale. Set WORKERS to your plan's number. Going over returns HTTP 429, which the script retries.
  • Cost control: set premium=False for sites that don't block datacenter IPs. That cuts the cost per page from 25 credits to 5.
  • Parsing: the saved HTML is complete, so parse it with any HTML library, or feed it to an extraction step. Our Zyte auto-extraction guide compares selectors with AI extraction.

3. What 1,000 rendered pages cost on ZenRows

ZenRows sells monthly credits and charges per successful request: 1 credit for a basic request, 5 with JavaScript rendering, 10 with premium proxies, and 25 with both. Failed requests aren't charged. Note that 404 and 410 responses count as successful.

Plan (billed monthly)Credits per monthConcurrencyPer 1,000 pages, JS renderingPer 1,000 pages, JS + premium proxyProtected pages per month
Free, $05,0005Enough to test: 1,000 rendered or 200 protected pages200
Build, $1945,00020$2.11$10.561,800
Launch, $69250,00050$1.38$6.9010,000
Growth, $1991,200,000100$0.83$4.1548,000
Scale, $5495,000,000200 to 400$0.55$2.75200,000

Prices from ZenRows' pricing page, October 2026. Each plan also comes in larger credit sizes. Annual billing gives two months free, about 17% less. The per-1,000 figures are our arithmetic from the plan prices.

4. The self-hosted alternative: Playwright with stealth patches

If you'd rather run the browsers yourself, the standard setup is Playwright plus the free stealth plugin from the puppeteer-extra project, behind residential proxies:

// npm install playwright playwright-extra puppeteer-extra-plugin-stealth
const { chromium } = require('playwright-extra');
chromium.use(require('puppeteer-extra-plugin-stealth')());

(async () => {
  const browser = await chromium.launch({
    proxy: { server: 'http://gate.your-proxy.example:7000', username: 'USER', password: 'PASS' },
  });
  const page = await browser.newPage();
  await page.goto('https://example-store.com/products/blue-mug', { waitUntil: 'networkidle' });
  console.log(await page.content());
  await browser.close();
})();

It works on many sites, and it's free. The catches:

  • The stealth plugin is a fixed set of patches. It hides well-known automation signs such as navigator.webdriver. Anti-bot vendors update their checks continually, and the plugin's last npm release (2.11.2) was in April 2023. When a target starts blocking you, finding and patching the new signal is your job.
  • Stealth doesn't fix your IP. Datacenter IPs are blocked on sight by many protected stores, so you still need residential proxies, billed per GB. A rendered page pulls in scripts, styles and API calls even with images blocked.
  • Browsers need memory. Each headless Chromium uses hundreds of MB, so concurrency is limited by RAM. Our self-hosted Playwright cluster guide covers sizing a VPS, queueing jobs and recycling browsers, and the proxy monitoring guide covers tracking success rate and cost per working request.

If you want to keep your Playwright code but stop running the browsers, there's a middle path: hosted Chrome over CDP. See our Browserless vs VPS cluster guide.

5. Cost comparison: 10,000 protected pages a month

A store behind bot protection, 10,000 product pages a month, all needing JavaScript rendering and residential IPs.

CostZenRowsSelf-hosted Playwright
Plan or server$69, Launch plan (250,000 credits = 10,000 pages at 25 credits)$24, a 4 GB DigitalOcean Droplet
Residential proxy bandwidthIncluded$15 to $140: 0.5 to 2 MB per rendered page at $3 to $7 per GB (our estimate)
Keeping stealth and unblocking workingNone3 to 6 hours a month at $50 an hour, $150 to $300 (our estimate)
Monthly total$69about $190 to $460
Monthly cost: 10,000 protected, JavaScript-rendered pages
Monthly cost: 10,000 protected, JavaScript-rendered pagesZenRows, Launch plan: 69 $/month; Self-hosted, low estimate: 189 $/month; Self-hosted, high estimate: 464 $/monthZenRowsSelf-hosted PlaywrightZenRows, Launch plan: 69 $/monthZenRows, Launch plan$69Self-hosted, low estimate: 189 $/monthSelf-hosted, low estimate$189Self-hosted, high estimate: 464 $/monthSelf-hosted, high estimate$464
ZenRows at list price, billed monthly. Self-hosted figures are our estimates: a $24 VPS, residential bandwidth at 0.5 to 2 MB per page and 3 to 6 hours of maintenance a month at $50 an hour.

When self-hosting wins

  • High volume on sites that don't block you. One million JavaScript-rendered pages a month without premium proxies is 5 million credits, $549 on ZenRows' Scale plan. A couple of 8 GB servers at about $48 each render that comfortably, and an unprotected site needs no residential bandwidth and little maintenance.
  • You need to interact with the page. Logging in, filling forms or clicking through multi-step flows is easier with your own Playwright session. ZenRows supports scripted JavaScript instructions, but a real browser session gives you full control.
  • You already run the infrastructure. If your team maintains a scraping cluster anyway, adding one more target is cheap.

On protected sites, compare the proxy bill alone first. At 1 MB per rendered page and $3 to $7 per GB, residential bandwidth costs $3 to $7 per 1,000 pages before any server or maintenance time, against $2.75 to $10.56 per 1,000 on ZenRows, which already includes the IPs. Self-hosting a protected target only pays off at large volume, with light pages and cheap bandwidth.

6. Which one to pick

Your situationBest fit
Protected stores, up to a few hundred thousand pages a monthZenRows with js_render and premium_proxy
JavaScript sites without bot protection, moderate volumeZenRows with js_render only, 5 credits a page
Millions of pages from unprotected sitesSelf-hosted Playwright on your own servers
Logged-in or interactive scraping, existing Playwright codeYour own Playwright, on your servers or on hosted Chrome

7. Tools

Top picks

ToolRoleWhy it fits
ZenRowsRendering and anti-bot scraping APIOne GET request returns rendered HTML from protected, JavaScript-heavy stores. Simple credit pricing, failed requests free, and a free plan with 5,000 credits to test your own targets before you pay.
ScraperAPIAlternative scraping APIThe same idea with different pricing: render=true costs 10 credits and premium=true with rendering 25 credits, with plans from $49 a month for 100,000 credits. Its pricing page adds 10 credits per request on sites behind Cloudflare, DataDome or PerimeterX. Worth comparing on your own target list.

Alternatives: free tools

We don't earn anything from these links.

  • Playwright: the browser automation library behind the self-hosted approach.
  • puppeteer-extra-plugin-stealth: the free stealth patches, usable from Playwright through playwright-extra. Expect to maintain your own fixes on top of it.

8. Pitfalls

  • Turning on premium proxies everywhere. Test each site with plain requests first, then js_render, then premium. Only pay 25 credits where 5 doesn't work.
  • Exceeding concurrency. More parallel requests than your plan allows come back as 429s. Match your worker count to the plan.
  • Assuming 404s are free. ZenRows bills 404 and 410 responses as successful. Clean dead URLs out of your lists.
  • Counting only the server bill when self-hosting. Residential bandwidth and the hours spent on blocks are usually the larger costs.
  • Ignoring a site's terms. Scrape public pages at a polite rate and check the site's terms of service.
Bottom line

On JavaScript-heavy, bot-protected stores, ZenRows replaces a browser cluster, a residential proxy bill and ongoing stealth maintenance with one API call, at about $2.75 to $10.56 per 1,000 protected pages depending on plan. For 10,000 protected pages a month that's $69, against an estimated $190 to $460 for doing it yourself. Run your own Playwright when you scrape millions of pages from sites that don't block you, or when you need full control of an interactive session.

David Lewin
David Lewin

Founder of StealthScale.io and e-commerce infrastructure engineer writing field-tested playbooks on proxies, cloud VPS, anti-detect browsers and SEO.