Browshot Alternative: From Polling to One Request

A Browshot alternative for teams that want one synchronous call instead of create and poll: parameter map, instance-to-device mapping and tested migration code.

By Simon Chiu · Updated

Disclosure: I build RenderScreenshot. Browshot has been around since 2011 and does things we do not, such as non-Chromium browser instances and multi-step automation. This page covers those honestly.

The usual reason to look for a Browshot alternative is the request model. Browshot's complete API is asynchronous: you call screenshot/create, poll screenshot/info until the status is finished or error, then download the image or a thumbnail. The simple endpoint hides that loop behind HTTP redirects, but your client still waits through them. RenderScreenshot answers a capture in the same HTTP response, and moves the long-running cases to batch jobs and webhooks. This page shows how each part of a Browshot integration maps across: parameters, instances, the poll loop, multi-URL requests and hosting. It is a spoke of our best screenshot API comparison.

Last verified: October 15, 2026, against the Browshot API documentation. Browshot's pricing page returned HTTP 403 to our checks, so this page quotes no Browshot prices.

Should you switch?

Stay with Browshot if Move to RenderScreenshot if
You need screenshots from non-Chromium browsers or specific device instances Chromium output is what you need, with mobile emulation when required
You script multi-step sessions (steps) or extract data with trackers You want one request, one image, no poll loop
You send POST data or a referrer on the first request You want signed, expiring URLs for public <img> tags
You prefer pay-as-you-go credits with no subscription You want presets for social cards and a monthly plan
You want wait conditions (network idle, selector) instead of a fixed 5 s delay

From create-and-poll to one call

A typical Browshot integration has three moving parts. Here is where each one goes:

Browshot What it does RenderScreenshot
GET /api/v1/screenshot/create Queues a capture, returns {"status":"in_process","id":...} or a cached result POST /v1/screenshot returns the image in the response
GET /api/v1/screenshot/info?id= (polled) Status: in_queue, processing, finished, error Not needed. Errors arrive as HTTP status codes with a typed JSON body
GET /api/v1/screenshot/thumbnail?id= Downloads the image or a resized thumbnail output.resize in the same request; the image is the response body
GET /api/v1/simple One URL that redirects (302) until the image is ready GET /v1/screenshot?url=... with a key or a signed URL
hook= parameter POST to your URL when finished or failed webhook.url with signed payloads
screenshot/multiple Up to 10 URLs × 10 instances in one call POST /v1/batch with per-URL options
hosting=s3 / hosting=browshot Upload to your S3 bucket or Browshot's CDN storage (your bucket, set up in the dashboard) or the public X-Cache-URL

The poll loop disappears, and so does the code around it: retry timers, maximum attempts, and the state you kept per screenshot ID. Keep a timeout on the single request instead. Our wait.timeout can go up to 60 seconds; set your HTTP client a little higher.

Parameter map

Browshot RenderScreenshot Notes
key (query) Authorization: Bearer rs_live_... Or a signed URL for public use
url url
instance_id viewport.* or viewport.device See the next section
size=screen / size=page capture.mode: viewport / full_page full_page preset adds scroll simulation
screen_width (1 to 5000), screen_height (1 to 10000) viewport.width (1 to 7680), viewport.height (1 to 4320) Full-page height goes up to capture.scroll.max_height (default 16,000)
delay (seconds, default 5) wait.delay (milliseconds, default 0) Prefer wait.until or wait.for_selector
max_wait wait.timeout (seconds, max 60)
target (CSS selector) capture.mode: element + capture.selector
hide_popups block.cookie_banners, block.ads
dark browser.dark_mode
cache (seconds, default 24 h, 0 = fresh) cache.ttl (3600 to 2,592,000, default 86,400), cache.refresh Our minimum TTL is 1 hour
headers network.headers
cookie (paid instances) network.cookies Array of cookie objects
script (URL), script_inline page.scripts (string or {url})
referer, post_data network.headers for a Referer; no POST navigation Keep Browshot if you need POST data
strict_ssl not available Certificate failures return the ssl_error code
html=1 output.format: html, or metadata.include
hook webhook.url, webhook.events
steps, trackers not available Multi-step automation is out of scope for us
thumbnail width, height, ratio=fit/fill output.resize.width, .height, .fit (contain, cover, ...) fit ≈ contain, fill ≈ cover
thumbnail format (png, jpeg) output.format (png, jpeg, webp, avif, pdf)

Instances become devices

Browshot's model starts from an instance: a browser with a screen size and user agent, numbered (instance 12 is the free one; the docs use 65 as an example premium instance). We start from the page and let you describe the device per request:

  • Desktop instances: set viewport.width and viewport.height, or use the desktop_hd (1920x1080) preset.
  • Mobile instances: use a device preset. GET /v1/devices lists them; built-ins include iphone_14_pro, iphone_14_pro_max, pixel_7, ipad_pro_12 and macbook_pro_14. A device sets viewport, scale, mobile and touch together.
  • Retina: viewport.scale from 1 to 3.

The trade-off is real: every RenderScreenshot capture is Chromium. If you used Browshot specifically to see a page in another browser engine, read our Chromium vs Firefox vs WebKit comparison and keep a tool that runs those engines.

The five-second default

Browshot waits delay=5 seconds after the page loads, by default. That makes captures reliable on slow pages and adds five seconds to every fast one. When you compare the two services, compare like with like: send delay=0 to Browshot, or wait.delay: 5000 to us. Better, replace the fixed wait on both sides with a condition. For client-rendered apps, wait for a selector that only exists once the data is on the page; our guide to screenshots of single-page applications covers the patterns.

We have not run Browshot through our benchmark, so there are no latency numbers or side-by-side images on this page yet. The Python script below runs both services on the same URL and saves both images, so you can measure on your pages.

Migration code

Node: replace the poll loop

// replace-poll.mjs: the Browshot create -> poll -> download loop becomes one call.
// Node 22+, npm i renderscreenshot
// Usage: RS_API_KEY=rs_live_... node replace-poll.mjs https://example.com
import { Client, TakeOptions, RenderScreenshotError } from 'renderscreenshot';
import { writeFile } from 'node:fs/promises';

const client = new Client(process.env.RS_API_KEY, {
  timeout: 60_000,
  ...(process.env.RS_BASE_URL ? { baseUrl: process.env.RS_BASE_URL } : {}),
});

// Map the Browshot arguments you already store to TakeOptions.
// Browshot waits 5 s after load by default (delay=5). RenderScreenshot waits for
// the load event and adds no delay unless you ask, so carry the delay over only
// if your pages need it. Better: wait for a selector that proves the page is ready.
export function fromBrowshot(b) {
  let o = TakeOptions.url(b.url);
  if (b.screen_width) o = o.width(Number(b.screen_width));
  if (b.screen_height) o = o.height(Number(b.screen_height));
  if (b.size === 'page') o = o.fullPage();
  if (b.target) o = o.element(b.target);              // CSS selector of one element
  if (b.hide_popups) o = o.blockCookieBanners().blockAds();
  if (b.dark) o = o.darkMode();
  if (b.delay !== undefined) o = o.delay(Number(b.delay) * 1000); // seconds -> ms
  if (b.cache !== undefined && Number(b.cache) >= 3600) o = o.cacheTtl(Number(b.cache)); // RS minimum TTL is 1 hour
  if (b.mobile) o = o.device('iphone_14_pro');      // instead of a mobile instance_id
  return o;
}

try {
  const t0 = performance.now();
  const image = await client.take(fromBrowshot({ url: process.argv[2] ?? 'https://example.com', size: 'page', screen_width: 1280, hide_popups: true, delay: 1 }));
  await writeFile('page.png', image);
  console.log(`saved ${image.length} bytes in ${Math.round(performance.now() - t0)} ms`);
} catch (err) {
  if (err instanceof RenderScreenshotError && err.retryable) console.error(`retryable: ${err.code}`);
  else console.error(err.message);
  process.exitCode = 1;
}

Python: both services, same URL

"""simple_vs_rs.py: Browshot's /api/v1/simple next to the RenderScreenshot call that replaces it.

Python 3.10+, standard library only. urllib follows the 302 redirects that
Browshot's simple API sends while a screenshot is still in progress.
Usage: BROWSHOT_KEY=... RS_API_KEY=rs_live_... python3 simple_vs_rs.py https://example.com
"""
import json
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request

BROWSHOT = os.environ.get("BROWSHOT_BASE", "https://api.browshot.com")
RS = os.environ.get("RS_BASE_URL", "https://api.renderscreenshot.com")


def browshot(url: str) -> bytes:
    query = urllib.parse.urlencode({
        "url": url, "key": os.environ["BROWSHOT_KEY"],
        "instance_id": os.environ.get("BROWSHOT_INSTANCE", "12"),  # 12 is the free instance
        "size": "page", "screen_width": 1280, "screen_height": 800, "delay": 0,
    })
    with urllib.request.urlopen(f"{BROWSHOT}/api/v1/simple?{query}", timeout=180) as res:
        return res.read()


def renderscreenshot(url: str) -> bytes:
    body = {
        "url": url,
        "viewport": {"width": 1280, "height": 800},
        "capture": {"mode": "full_page", "scroll": {"simulate": True}},  # scroll to trigger lazy images
        "wait": {"until": "networkidle", "timeout": 30},
    }
    req = urllib.request.Request(
        f"{RS}/v1/screenshot", data=json.dumps(body).encode(), method="POST",
        headers={"Authorization": f"Bearer {os.environ['RS_API_KEY']}", "Content-Type": "application/json"},
    )
    with urllib.request.urlopen(req, timeout=90) as res:
        return res.read()


def main(url: str) -> int:
    status = 0
    for name, fn in (("browshot", browshot), ("renderscreenshot", renderscreenshot)):
        start = time.perf_counter()
        try:
            data = fn(url)
        except urllib.error.HTTPError as err:
            # Browshot puts the reason in the X-Error header.
            print(f"{name}: HTTP {err.code} {err.headers.get('X-Error', '')}", file=sys.stderr)
            status = 1
            continue
        ms = round((time.perf_counter() - start) * 1000)
        with open(f"{name}.png", "wb") as fh:
            fh.write(data)
        print(f"{name}: {len(data)} bytes in {ms} ms")
    return status


if __name__ == "__main__":
    sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "https://example.com"))

Browshot answers 404 with a "not found" image when a capture fails, and puts the reason in the X-Error header. urllib raises on the 404, so the script reports it instead of saving the placeholder as if it were a screenshot.

Ruby: screenshot/multiple as one batch

# multiple_to_batch.rb: Browshot's screenshot/multiple (URLs x instances) as one RenderScreenshot batch.
# gem install renderscreenshot
require 'renderscreenshot'

client = RenderScreenshot.client(ENV.fetch('RS_API_KEY'), base_url: ENV['RS_BASE_URL'])

urls = ARGV.empty? ? ['https://example.com', 'https://example.org'] : ARGV
# One Browshot instance per device becomes one request per device here.
devices = { 'desktop' => { width: 1280, height: 800 }, 'mobile' => { device: 'iphone_14_pro' } }

requests = urls.product(devices.to_a).map do |url, (_name, opts)|
  { url: url, options: opts }
end

result = client.batch_advanced(requests)
puts "batch #{result['id']}: #{result['status']}"
Array(result['results']).each do |r|
  puts "#{r['url']} #{r['status']} #{r.dig('image', 'url') || r.dig('error', 'code')}"
end

Batches of 10 URLs or fewer return results in the same response; larger batches return a batch ID to check with GET /v1/batch/:id or a webhook. Batch size limits depend on your plan.

All three scripts tested on October 15, 2026 against a local mock that follows Browshot's redirect behaviour and records RenderScreenshot request bodies: Node 22.18.0 with [email protected], Python 3.14.8, Ruby 4.0.7 with the renderscreenshot gem 1.0.0. Not run against either production API.

Rollout

  1. Find every place that polls. Search for screenshot/info and simple. Each call site becomes one request.
  2. Translate stored options with fromBrowshot, and decide per call site whether the 5 s delay was doing real work.
  3. Run both for a sample with the Python script, and compare the saved images.
  4. Move hooks to webhooks. If you used hook=, point webhook.url at the same handler and verify the signature with the SDK's webhook helper.
  5. Delete the poll code after a clean week.

When Browshot is the better choice

Keep Browshot for jobs that need what we deliberately do not build: screenshots from a specific browser instance other than Chromium, scripted multi-step sessions, trackers that extract prices or ranks while capturing, and POST requests as the first navigation. If those are a small part of your traffic, split it: keep Browshot for those jobs and move plain captures. If they are most of your traffic, Browshot is the right tool and this page should not change your mind.

For everything else, create a free key, translate one stored Browshot request with the Node function above, and compare the result with what Browshot returns today.

Frequently asked questions

Do I still need to poll for the screenshot after moving from Browshot?
No. A RenderScreenshot POST /v1/screenshot request returns the image (or JSON with a CDN URL) in the same response. For long jobs or many URLs, use the batch endpoint or a webhook instead of a poll loop.
What replaces Browshot's instance_id?
Browshot instances are browser and device configurations. On RenderScreenshot you choose the viewport, scale and mobile emulation per request, or a device preset such as iphone_14_pro or pixel_7. The renderer is always Chromium.
Why were my Browshot screenshots slow?
Browshot's screenshot/create waits 5 seconds after the page has loaded unless you pass delay=0, and the simple API answers with redirects while a capture is in progress. Set delay=0 and wait for a selector instead where you can.
Is there a Browshot feature RenderScreenshot does not have?
Yes. Browshot offers non-Chromium browser instances, multi-step automation (steps), data trackers, POST data on the first request and URL classification. If you rely on those, keep Browshot for those jobs.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.