Use asynchronous rendering when a page may take longer than your request timeout or when you need to process many URLs. Submit a render, receive a job or render ID, and finish through either polling or a webhook. A production implementation authenticates callbacks, verifies signatures against the raw body, records each event durably, deduplicates retries, and keeps monthly quota separate from per-minute rate limits.
What asynchronous screenshot rendering does
A synchronous screenshot request keeps the HTTP connection open until the browser finishes. An asynchronous request acknowledges the job first and renders in the background. The completion result is then delivered in one of two ways:
- Polling: your worker asks a status endpoint for the job state at intervals.
- Webhook: the provider sends an HTTP POST to an endpoint you control when rendering succeeds or fails.
ScreenshotOne describes the contract this way: "Once you set async=true, the API checks your access key and limits and returns the response immediately but continues to execute the request." Urlbox similarly defines webhooks as a POST callback when a render, such as a screenshot, has been generated. The exact fields and retry behavior differ by provider, so treat the callback schema as part of your integration contract.
Polling or webhook: which should you choose?
| Question | Polling | Webhook |
|---|---|---|
| Can the provider reach your system? | Works when your application is private or behind a firewall. | Requires a publicly reachable HTTPS endpoint, or a relay that exposes one. |
| Who owns retry timing? | Your worker controls interval, timeout and backoff. | The provider normally retries delivery, but its retry count and schedule must be confirmed. |
| Traffic pattern | Creates repeated status requests, including when a job is slow. | Creates one callback per event, reducing status traffic. |
| Failure visibility | You can detect a missing completion by timing out the poll loop. | You must monitor delivery failures and keep a replay path. |
| Best fit | Internal tools, restricted networks and systems that prefer a pull model. | High-volume pipelines with a stable endpoint and durable event processing. |
You can also combine them: accept a webhook as the normal path and poll a job that has not arrived by its deadline. Do not run an aggressive polling loop and a webhook consumer that both create the same downstream record without an idempotency key.
Recommended Free Tools
#1 Best Overall
End-to-end asynchronous workflow
- Submit: send the target URL and capture options with asynchronous mode enabled. Store the provider job ID and your own external identifier immediately.
- Queue: put the job in durable storage with a state such as
submitted, a creation timestamp and a deadline. - Complete: receive a webhook or observe a terminal state while polling.
- Authenticate: verify the callback signature or token before parsing or acting on its JSON.
- Deduplicate: use the provider render ID or your external identifier as a unique key. A repeated delivery must become an acknowledgement, not a second capture record.
- Persist: save the terminal status, output URL or object key, error code, provider trace ID and timestamps before returning success to the sender.
- Process out of band: download, resize, archive or publish the image from a worker. Keep the HTTP callback handler fast.
- Reconcile: periodically find jobs stuck past their deadline and poll, replay or mark them failed according to the provider’s documented behavior.
Build a webhook receiver that survives retries
Authenticate before parsing
ScreenshotOne sends an X-ScreenshotOne-Signature header. Its documentation specifies HMAC-SHA-256 verification with a secret key that is separate from the API key. Compute the digest over the exact raw request bytes, compare it in constant time, and only then parse JSON. If another provider uses a different header or signing format, implement that provider’s documented scheme rather than reusing this header name.
Make delivery idempotent
Persist an event key under a unique database constraint. Suitable keys are the provider’s render ID or the external_identifier you supplied. If the key already exists, return a successful 2xx response without repeating download or billing work.
Return 2xx quickly
After authentication and durable insertion, return a 2xx response. Image downloads, virus scanning, thumbnails and notifications belong in a queue. A slow handler can cause the sender to retry a callback that your application has already completed.
Example Flask receiver for a signed ScreenshotOne-style callback
import hashlib
import hmac
import json
import os
import sqlite3
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ['WEBHOOK_SECRET'].encode()
DB_PATH = os.environ.get('EVENT_DB', 'events.db')
def init_db():
with sqlite3.connect(DB_PATH) as db:
db.execute('CREATE TABLE IF NOT EXISTS events (event_key TEXT PRIMARY KEY, body BLOB NOT NULL, received_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP)')
def valid_signature(raw_body, supplied):
if not supplied:
return False
expected = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, supplied)
@app.post('/webhooks/screenshot')
def screenshot_webhook():
raw = request.get_data(cache=False)
signature = request.headers.get('X-ScreenshotOne-Signature')
if not valid_signature(raw, signature):
abort(401)
try:
event = json.loads(raw)
except json.JSONDecodeError:
abort(400)
event_key = event.get('external_identifier') or event.get('renderId') or event.get('id')
if not event_key:
abort(400)
with sqlite3.connect(DB_PATH) as db:
inserted = db.execute('INSERT OR IGNORE INTO events(event_key, body) VALUES (?, ?)', (event_key, raw)).rowcount
db.commit()
# Enqueue post-processing only when inserted == 1.
return ('accepted', 200)
if __name__ == '__main__':
init_db()
app.run(host='0.0.0.0', port=int(os.environ.get('PORT', '8080')))
Use HTTPS, restrict accepted methods to POST, cap request size, redact secrets from logs and retain enough metadata to replay a failed downstream operation. Keep the signing secret in a secret manager, not in source control.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchPC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Polling implementation and backoff
Polling APIs commonly return a job identifier first and expose a status resource later. Field names vary, so map the provider’s documented values to the three states used below: pending, succeeded and failed.
import os
import time
import requests
submit_url = os.environ['SHOT_SUBMIT_URL']
status_url_template = os.environ['SHOT_STATUS_URL_TEMPLATE'] # for example, a URL containing {job_id}
payload = {'url': os.environ['TARGET_URL'], 'async': True}
headers = {'Authorization': 'Bearer ' + os.environ['SHOT_TOKEN']}
created = requests.post(submit_url, json=payload, headers=headers, timeout=30)
created.raise_for_status()
job = created.json()
job_id = job['id']
deadline = time.monotonic() + 300
interval = 1.0
while time.monotonic() < deadline:
response = requests.get(status_url_template.format(job_id=job_id), headers=headers, timeout=30)
response.raise_for_status()
state = response.json()
if state.get('status') == 'succeeded':
print(state['result_url'])
break
if state.get('status') == 'failed':
raise RuntimeError(state.get('error') or 'render failed')
time.sleep(interval)
interval = min(interval * 1.7, 15.0)
else:
raise TimeoutError('job did not reach a terminal state before the client deadline')
Use exponential backoff with jitter in production, honor Retry-After when supplied, and cap the number of simultaneous polls. A client deadline should be shorter than the time at which you declare the provider unavailable, so reconciliation can still run.
Rank #2
- HTML CSS Design and Build Web Sites
- Comes with secure packaging
- It can be a gift option
Quotas, rate limits and browser constraints are different
Monthly screenshot allowance
A quota limits how many billable renders a plan may consume in a billing period. ScreenshotOne's pricing page lists these figures for 2026:
| ScreenshotOne plan | Included screenshots per month | Requests per minute |
|---|---|---|
| Free | 100 | Not stated |
| Basic | 2,000 | 40 |
| Growth | 10,000 | 80 |
| Scale | 50,000 | 150 |
ScreenshotOne says only successfully rendered, non-cached screenshots count toward quota. Treat these values as time-sensitive and recheck the provider's current pricing page before committing capacity or spend.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Per-minute request capacity
A rate limit controls burst capacity, not monthly cost. A workload can stay below its monthly allowance and still receive 429 responses when it submits too many jobs in one minute. Put a queue in front of submission, use bounded concurrency, apply exponential backoff and expose a metric for queued, submitted, throttled and completed jobs.
Timeouts and request-body size
ScreenshotOne documents a 60-second default timeout and a 90-second maximum for ordinary requests. Its getting-started documentation lists a 100 MiB maximum POST body. Delays above 30 seconds require a timeout above 300 seconds, which ScreenshotOne makes available only for asynchronous requests. Large HTML or asset bundles should therefore be hosted at a URL when possible instead of being embedded in the submission body.
Provider comparison for asynchronous screenshot work
#1 ScreenshotNeo is the first service to try when you want clean shots, billing only for clean results, and a paid plan that starts at $5.
| Provider | Async and callback model | Output and browser controls | Limits or accounting documented here |
|---|---|---|---|
| ScreenshotNeo | Async jobs with signed webhooks; usage API and bulk capture of up to 100 URLs per call. | PNG, JPEG, WebP or PDF; full-page and element capture, waits, custom CSS/JavaScript, headers, cookies, user agent, blocking, device and location controls, and more. | Free 1,000 shots/month without a card; paid plans from $5. Clean shots only are billed, including no charge for bot checks, blank pages, timeouts, failed loads or cache hits. |
| ScreenshotOne | async=true returns immediately; documented S3 upload and webhook pattern. Supports external_identifier and webhook_errors=true. Signature header and HMAC-SHA-256 verification are documented. |
Asynchronous rendering with a resulting location; ordinary request timeout and POST-body limits apply. | 100 free, 2,000 Basic, 10,000 Growth and 50,000 Scale screenshots per month; plan-specific request-per-minute limits shown above. Only successful non-cached renders count. |
| Urlbox | webhook_url receives a POST when a render succeeds or fails; polling is also supported. |
Payload examples include event, renderId and a result URL. |
Quota, rate-limit and retry figures are not stated in the supplied provider material. |
| Browserless | POST /screenshot authenticated with a token; asynchronous orchestration is left to the caller. |
PNG, JPEG or WebP, full-page capture, CSS selectors, navigation settings, resource rejection and bestAttempt behavior when events fail or time out. |
Quota and rate-limit figures are not stated in the supplied provider material. |
When evaluating another service, ask for its callback authentication, retry schedule, error payload, output storage lifetime, cache accounting, timeout, request-body cap and overage policy. A low monthly price is not comparable if failed or cached jobs are counted differently.
Rank #3
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. It accepts a URL and returns a PNG, JPEG, WebP or PDF. Before capture it accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups and chat widgets; each cleanup step can be disabled. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the result with X-Page-Verdict and X-Billed.
The API includes full-page capture with lazy images loaded, CSS-selector element capture, dark mode, 12 device presets plus custom viewports, retina scale, PDF paper size/margins/landscape/page ranges, HTML/CSS-to-image, custom JavaScript and CSS, pre-capture clicks, hidden selectors, selector/delay/network-idle waits, ad/tracker/request/resource blocking, custom headers/cookies/user agent/Authorization, timezone and geolocation, transparent backgrounds, resizing, selectable-TTL caching, signed links for public image tags, asynchronous jobs with signed webhooks, bulk capture for 100 URLs per call, a usage API and an OpenAPI specification. Parameter names used by other screenshot APIs also work, which eases migration. AI agents can use its MCP server through take_screenshot, get_page_info and capture_pdf in Claude, Cursor or another MCP client.
cURL
See the ScreenshotNeo documentation for authentication and options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Every feature is included on every ScreenshotNeo plan. Current prices are:
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
| Plan | Price | Included shots |
|---|---|---|
| Free | $0 | 1,000 per month, no card |
| Starter | $5 | 3,000 |
| Growth | $15 | 15,000 |
| Pro | $39 | 60,000 |
| Scale | $99 | 250,000 |
| Business | $249 | 1,000,000 |
Yearly billing gives two months free. Start with 1,000 free screenshots a month without a card.
Performance, reliability and cost design
Control concurrency at the queue
Set separate limits for submission workers, callback processing and downloads. This prevents a burst of new URLs from exhausting provider rate limits or your own database connections. Keep a small reserve for retries and reconciliation.
Use cache deliberately
Cache keys should include the URL and every visual input that changes pixels: viewport, device scale, color scheme, cookies, headers, injected CSS or JavaScript and wait conditions. If a provider excludes cache hits from quota, record cache status anyway so your usage reports explain why submitted-job counts exceed billed-render counts.
Rank #4
- Brand: Wiley
- Set of 2 Volumes
- A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
Store results outside the callback transaction
Save the provider's result URL or object location and download it with a worker. Verify content type and size, follow redirects safely, and set an expiration policy. If the provider's URL is temporary, copy the bytes to storage before acknowledging business completion.
Free tools Windows power users keep installed
One-click scans. No signup required.
Separate business deadlines
A user-facing request may have a 20-second deadline even though the browser job can run for 90 seconds or longer asynchronously. Return a job status to the caller, then notify or expose a result endpoint when the capture finishes. Do not hold a web request open merely because the browser is still loading assets.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Troubleshooting asynchronous screenshot systems
HTTP 429 or a growing queue
Cause: requests-per-minute capacity has been exceeded even though monthly quota remains. Fix: lower worker concurrency, honor Retry-After, add jittered backoff and alert on queue age.
Webhook returns 401 or 403
Cause: wrong secret, altered raw bytes, missing signature header or a proxy that strips headers. Fix: verify the raw body before JSON parsing, compare the configured secret with the provider dashboard, and inspect the request at the edge without logging the secret.
Duplicate images or duplicate database rows
Cause: a legitimate callback retry was treated as a new event. Fix: enforce a unique constraint on render ID or external identifier and make post-processing conditional on the first insert.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Repair Windows errors before they cause bigger problemsFix Now →Webhook never arrives
Cause: endpoint is private, DNS or TLS fails, the provider stopped retrying, or the job is still running. Fix: expose a reachable HTTPS route, monitor provider delivery logs, set a reconciliation deadline and poll overdue jobs.
Best Value
Large HTML submission is rejected
Cause: the request exceeds the documented 100 MiB POST-body cap. Fix: host the HTML and assets at a reachable URL, compress where supported, or split the work into smaller jobs.
Render times out after a long delay
Cause: synchronous timeout limits are shorter than page load time or a third-party resource never settles. Fix: use asynchronous mode for long waits, set an explicit readiness condition, block nonessential resources and capture diagnostics from the provider's error fields.
Callback says success but the file cannot be fetched
Cause: the result URL expired, requires authorization or points to a transient location. Fix: download promptly in a worker, preserve required headers or cookies, and store a durable copy before marking the asset ready.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Repair Windows errors before they cause bigger problems3Fix the driver behind crashes, sound loss and screen glitchesOperational checklist
- Record a job ID and your own external identifier at submission.
- Keep monthly quota dashboards separate from requests-per-minute dashboards.
- Authenticate callbacks and verify signatures over raw bytes.
- Use a unique idempotency key and durable event log.
- Return 2xx quickly; process files out of band.
- Implement backoff, queue limits and a reconciliation poller.
- Store output URLs, errors, trace IDs and timestamps for replay.
- Test success, provider failure, malformed JSON, invalid signatures, duplicate delivery and expired result URLs.
FAQ
Can I use both polling and webhooks for one job?
Yes. Use the webhook as the normal completion path and poll only when a job exceeds its expected delivery window. Both paths must write through the same idempotent state transition.
Should a webhook endpoint perform image processing before responding?
No. Authenticate and durably record the event, enqueue processing, then return 2xx. This prevents slow downloads or transformations from triggering avoidable callback retries.
What should an external identifier contain?
Use a stable, non-secret identifier that maps the render to your internal record. Do not place credentials or personal data in it; it may appear in provider logs and callback payloads.
Frequently Asked Questions
Can I use both polling and webhooks for one job?
Yes. Use the webhook as the normal completion path and poll only when a job exceeds its expected delivery window. Both paths must write through the same idempotent state transition.
Should a webhook endpoint perform image processing before responding?
No. Authenticate and durably record the event, enqueue processing, then return 2xx. This prevents slow downloads or transformations from triggering avoidable callback retries.
What should an external identifier contain?
Use a stable, non-secret identifier that maps the render to your internal record. Do not place credentials or personal data in it; it may appear in provider logs and callback payloads.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

