Disclosure: RenderScreenshot is our product, and we compete with ScrapingBee only on screenshots. ScrapingBee is a web scraping API; screenshots are one of its outputs. All ScrapingBee facts on this page come from ScrapingBee's documentation, checked on 2026-10-15.
If you call ScrapingBee with screenshot=true, you use a scraping product to do a rendering job. That works, and for some teams it is the right call. It also means you pay scraping prices per request, configure the page through scraping parameters, and get a narrower set of image controls. This page measures that trade-off on screenshot tasks only. If your main job is extracting data, keep ScrapingBee.
For the full list of screenshot services, see our screenshot API comparison.
TL;DR
| Keep ScrapingBee if | Use a screenshot API if |
|---|---|
| You need the HTML, extracted fields and an image from the same request | You only need the image |
| Your targets block normal browsers and you need its premium or stealth proxies | Your targets are your own pages, customer sites or ordinary public pages |
| You already pay for a large credit balance that covers scraping and screenshots | You want one credit per screenshot, no matter which options you use |
You use js_scenario flows to log in or click before the capture |
You need output controls: WebP, AVIF, PDF, resize, transparency, presets |
Why the cost model matters
ScrapingBee charges credits per request, and the price of a request depends on how it is made. Its credit table lists:
| ScrapingBee configuration | Credits per request |
|---|---|
| Classic proxy, no JavaScript | 1 |
| Classic proxy, JavaScript rendering (the default) | 5 |
| Premium proxy, no JavaScript | 10 |
| Premium proxy, JavaScript rendering | 25 |
| Stealth proxy, JavaScript rendering | 75 |
Source: ScrapingBee documentation, "Credit cost for your requests", checked on 2026-10-15. A screenshot needs a rendered page, so the cheapest screenshot is 5 credits. To compare plans, divide a plan's monthly credits by the credits per screenshot you actually use:
screenshots per month = plan credits / credits per screenshot request = plan credits / 5 (classic proxy) = plan credits / 25 (premium proxy) = plan credits / 75 (stealth proxy)
The same balance also pays for your scraping calls, so screenshots and scraping compete for it. RenderScreenshot charges 1 credit per rendered screenshot. Blocking, full-page capture, dark mode and wait conditions do not change the price.
Cost at three volumes
RenderScreenshot prices as listed on /pricing, checked on 2026-10-15. For ScrapingBee, take the plan price from ScrapingBee's pricing page and apply the formula above; we did not reproduce their plan prices here because we could not load that page in our check.
| Screenshots per month | ScrapingBee credits needed (classic / premium proxy) | RenderScreenshot |
|---|---|---|
| 1,000 | 5,000 / 25,000 | Starter, $10/month (2,500 credits) |
| 10,000 | 50,000 / 250,000 | Growth, $40/month (12,000 credits) |
| 100,000 | 500,000 / 2,500,000 | More than Scale's 60,000 credits ($100/month); contact us |
At high volume on easy pages, a large ScrapingBee plan can cost less per image than a screenshot API. At premium-proxy rates the math reverses fast. Run the formula with your real proxy mix before you decide.
Parameter mapping
| Task | ScrapingBee | RenderScreenshot |
|---|---|---|
| Endpoint and auth | GET https://app.scrapingbee.com/api/v1/?api_key=... |
POST /v1/screenshot with Authorization: Bearer, or GET with a signed URL |
| Turn on a screenshot | screenshot=true |
Always an image (or PDF) |
| Full page | screenshot_full_page=true |
capture.mode: "full_page", capture.scroll.simulate: true for lazy images |
| One element | screenshot_selector=<css> |
capture.mode: "element", capture.selector |
| Viewport | window_width, window_height |
viewport.width, viewport.height, viewport.scale |
| Wait time | wait (ms) |
wait.delay (ms) |
| Wait for an element | wait_for=<css> |
wait.for_selector |
| Wait for a browser event | wait_browser |
wait.until: load, domcontentloaded, networkidle |
| Ads | block_ads=true |
block.ads: true |
| Cookie banners | Click it with js_scenario |
block.cookie_banners: true |
| Chat widgets, trackers | block_resources / custom JS |
block.chat_widgets, block.trackers |
| Run JS before capture | js_scenario |
page.scripts, page.click |
| Country / proxy pool | country_code, premium_proxy, stealth_proxy |
Not offered |
| Output format | PNG | output.format: png, jpeg, webp, avif, pdf |
| Dark mode, timezone, locale | Custom JS or headers | browser.dark_mode, browser.timezone, browser.locale |
| HTML of the page | json_response=true |
Not offered; use a scraping tool |
Our parameter references: capture, wait, blocking, network.
Measure both on your own URLs
We have not published head-to-head numbers yet, because a fair run needs a paid ScrapingBee plan, our shared test corpus, and the same day for both providers. The scripts below do that run for you. Each one requests the same full-page 1280 x 800 capture with ads blocked, times it, and checks that the response body really is an image (an error can come back as JSON or HTML, so the script checks the first bytes of the body as well as the status).
How to read the results:
- Use at least 5 URLs: a static page, a long page with lazy images, a page with a cookie banner, a single-page app, and the hardest page you capture in production.
- Run each URL 3 times or more. Report p50 and p95, not the average.
- Keep
cache.enabled: falseon our side, or you will measure our cache rather than a render. - Open the images. Check that the bottom of long pages rendered and that no banner covers the content.
Node.js (Node 22+)
// Same screenshot task through ScrapingBee and RenderScreenshot. // Prints latency, bytes, and the PNG/JPEG signature check for each provider. // Usage: SCRAPINGBEE_API_KEY=... RS_API_KEY=... node compare.mjs https://example.com const target = process.argv[2] ?? 'https://example.com'; const RUNS = Number(process.env.RUNS ?? 3); const providers = { async scrapingbee(url) { const q = new URLSearchParams({ api_key: process.env.SCRAPINGBEE_API_KEY, url, screenshot: 'true', screenshot_full_page: 'true', window_width: '1280', window_height: '800', block_ads: 'true', }); return fetch(`https://app.scrapingbee.com/api/v1/?${q}`, { signal: AbortSignal.timeout(90_000) }); }, async renderscreenshot(url) { return fetch('https://api.renderscreenshot.com/v1/screenshot', { method: 'POST', headers: { Authorization: `Bearer ${process.env.RS_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ url, viewport: { width: 1280, height: 800 }, capture: { mode: 'full_page', scroll: { simulate: true } }, block: { ads: true, cookie_banners: true }, cache: { enabled: false }, // measure renders, not cache hits }), signal: AbortSignal.timeout(90_000), }); }, }; const isImage = (b) => (b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47) || (b[0] === 0xff && b[1] === 0xd8); let failed = false; for (const [name, call] of Object.entries(providers)) { for (let i = 1; i <= RUNS; i++) { const t0 = performance.now(); try { const res = await call(target); const body = Buffer.from(await res.arrayBuffer()); const ms = Math.round(performance.now() - t0); const ok = res.ok && isImage(body); if (!ok) failed = true; console.log(JSON.stringify({ provider: name, run: i, status: res.status, ms, bytes: body.length, image: ok, credits: res.headers.get('x-credits-used') })); } catch (err) { failed = true; console.log(JSON.stringify({ provider: name, run: i, error: err.message })); } } } process.exitCode = failed ? 1 : 0;
credits reads our X-Credits-Used header, so it is empty for ScrapingBee rows. Use the credit table above for those.
Python
"""Same screenshot task through ScrapingBee and RenderScreenshot (Python 3.9+, requests).""" import json import os import sys import time import requests TARGET = sys.argv[1] if len(sys.argv) > 1 else "https://example.com" def scrapingbee(url): return requests.get("https://app.scrapingbee.com/api/v1/", params={ "api_key": os.environ["SCRAPINGBEE_API_KEY"], "url": url, "screenshot": "true", "screenshot_full_page": "true", "window_width": 1280, "window_height": 800, "block_ads": "true", }, timeout=90) def renderscreenshot(url): return requests.post("https://api.renderscreenshot.com/v1/screenshot", headers={"Authorization": f"Bearer {os.environ['RS_API_KEY']}"}, json={"url": url, "viewport": {"width": 1280, "height": 800}, "capture": {"mode": "full_page", "scroll": {"simulate": True}}, "block": {"ads": True, "cookie_banners": True}, "cache": {"enabled": False}}, timeout=90) failed = False for name, call in (("scrapingbee", scrapingbee), ("renderscreenshot", renderscreenshot)): t0 = time.perf_counter() try: r = call(TARGET) ok = r.ok and (r.content[:4] == b"\x89PNG" or r.content[:2] == b"\xff\xd8") failed |= not ok print(json.dumps({"provider": name, "status": r.status_code, "ms": round((time.perf_counter() - t0) * 1000), "bytes": len(r.content), "image": ok, "credits": r.headers.get("x-credits-used")})) except requests.RequestException as e: failed = True print(json.dumps({"provider": name, "error": str(e)})) sys.exit(1 if failed else 0)
Ruby
# Same screenshot task through ScrapingBee and RenderScreenshot (Ruby 3+, stdlib only). require 'net/http' require 'json' target = ARGV[0] || 'https://example.com' def scrapingbee(url) uri = URI('https://app.scrapingbee.com/api/v1/') uri.query = URI.encode_www_form(api_key: ENV.fetch('SCRAPINGBEE_API_KEY'), url: url, screenshot: true, screenshot_full_page: true, window_width: 1280, window_height: 800, block_ads: true) Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: 90) { |h| h.get(uri.request_uri) } end def renderscreenshot(url) uri = URI('https://api.renderscreenshot.com/v1/screenshot') body = { url: url, viewport: { width: 1280, height: 800 }, capture: { mode: 'full_page', scroll: { simulate: true } }, block: { ads: true, cookie_banners: true }, cache: { enabled: false } } Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: 90) do |h| h.post(uri.path, body.to_json, 'Authorization' => "Bearer #{ENV.fetch('RS_API_KEY')}", 'Content-Type' => 'application/json') end end failed = false { 'scrapingbee' => method(:scrapingbee), 'renderscreenshot' => method(:renderscreenshot) }.each do |name, call| t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) res = call.call(target) ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000).round body = res.body.b ok = res.is_a?(Net::HTTPSuccess) && (body.start_with?("\x89PNG".b) || body.start_with?("\xFF\xD8".b)) failed ||= !ok puts({ provider: name, status: res.code.to_i, ms: ms, bytes: body.bytesize, image: ok, credits: res['x-credits-used'] }.to_json) end exit(failed ? 1 : 0)
Tested on 2026-10-15 with Node 22.18.0, Python 3.14 (requests 2.34.2) and Ruby 4.0.7 against a local mock that stands in for both APIs. The mock confirmed the query strings and JSON bodies. Real latency and image quality need your own keys.
Response and workflow differences
- What comes back. ScrapingBee returns the image bytes for
screenshot=true, or a JSON body with the screenshot and the HTML when you addjson_response=true. We return the image bytes with metadata in headers (X-Screenshot-Width,X-Screenshot-Height,X-Credits-Used,X-Cache-URL), or JSON with a CDN URL when you sendAccept: application/json. See response headers. - Embedding. ScrapingBee requests carry the API key in the query string, so they belong on your server. Our signed URLs carry only a public key ID, an expiry and an HMAC signature, so you can put them in an
<img>tag or anog:imagemeta tag. - Caching. Every ScrapingBee request is a fresh request. Our renders are cached for 24 hours by default (up to 30 days), and the
X-Cache-URLgives you a public CDN link to the stored image. See caching. - Batches. We accept up to 100 URLs per call on the largest plan through the batch endpoint. With ScrapingBee you run your own loop within its concurrency limit.
When to keep ScrapingBee
Keep it when the screenshot is a side product of scraping: you already fetch the page for data, and the image is evidence of what you scraped. One request then gives you both, and moving only the screenshot to another vendor adds a second call per page. Keep it also when your targets block datacenter traffic and you need its premium or stealth proxy pool. We do not run an anti-bot proxy tier.
Switch the screenshots when they are the product: link previews, social cards, archives, visual monitoring, reports. Then image controls, caching and a flat per-image price matter more than extraction. To check this on your own URLs, get a free key and run the comparison script.