Webhook vs Polling for Async Screenshot & PDF APIs
September 30, 2026


Why Async Jobs Need a Notification Strategy
A screenshot of a static marketing page might render in under two seconds. The same request against a heavy React SPA, or a PDF generation job spanning a hundred paginated sections, can take 30 seconds or more. If your architecture assumes a quick synchronous round-trip, you either time out on the slow jobs or over-provision timeouts everywhere and tie up connections waiting on the fast ones.
This is why an async job model exists: you submit the render request, get back a job_id immediately, and let the system tell you — or ask the system — when the result is ready. Async screenshot and PDF jobs both follow the same shape: submit, wait, retrieve. The part worth designing carefully is how your system learns the job is done. That decision determines your infrastructure footprint, your latency, and how gracefully you handle spikes in job volume when, say, a batch of a thousand PDFs lands at once.
Webhooks vs Polling: The Core Tradeoff
The webhook vs polling question comes down to who initiates the check and how much waiting that implies. A webhook is a callback: Browsevra POSTs to a URL you registered the moment the job finishes, success or failure. Polling means your server periodically hits a polling job status API endpoint asking "is it done yet?" until the answer is yes.
For rendering jobs specifically, the tradeoffs are concrete:
- Latency. Webhooks notify you within moments of completion. Polling latency depends entirely on your interval — poll every 10 seconds and you've added up to 10 seconds of lag per job, multiplied across every job in flight.
- Infrastructure cost. Webhooks require a publicly reachable endpoint, TLS, and a receiver that can handle inbound traffic reliably. Polling requires just an outbound HTTP client, but it burns request volume even when nothing has changed — a cost problem covered in more depth here.
- Firewall and network constraints. If your workers run in a locked-down environment with no inbound access, webhooks are a nonstarter without a relay layer.
- Failure visibility. A webhook tells you success or failure explicitly, once, per job. Polling gives you full visibility into intermediate states (queued, rendering, failed) on every request, useful for debugging but redundant for most production flows.
Neither pattern is universally correct — the right choice tracks with job volume, job duration variance, and how much control you have over your own network, a framework laid out well in this polling-vs-webhooks comparison.
When Polling Still Wins
Polling remains the better default in several situations. Low-volume use — a handful of PDFs a day — doesn't justify standing up and securing a webhook receiver. Prototyping and internal tools benefit from the simplicity of a synchronous-feeling loop you can debug with a terminal and curl. And plenty of legitimate environments — CI runners, serverless functions without persistent endpoints, corporate networks that block inbound traffic — simply can't accept a webhook reliably.
If you go this route, the implementation detail that matters most is interval strategy: use exponential backoff with jitter rather than a fixed interval, so you're not hammering the endpoint at a constant cadence while a slow SPA render is still in progress. A tight poll loop checking every 500ms wastes both money and rate-limit headroom; a loop that starts at 1-2 seconds and backs off as duration extends handles the 2-second and 30-second cases without wasting requests on either. The billing implications of polling frequency versus webhook delivery are worth understanding before you commit — see the billing math breakdown for how job-based pricing interacts with polling volume.
Designing a Reliable Webhook Pattern
A production-grade webhook pattern rests on four pillars, and skipping any one is where most integrations break in practice.
Signature verification. Every webhook payload should be signed with HMAC-SHA256 and verified against the raw request body — not a re-serialized JSON object, since re-serialization can silently change byte order and break the signature check. Compare signatures using a constant-time comparison function to avoid timing attacks. This step is non-negotiable if you want confidence that a payload actually came from Browsevra and not a spoofed request; the underlying mechanics are explained well in this HMAC and webhook security reference.
Idempotent handling. Because delivery is at-least-once, not exactly-once, the same webhook can arrive twice — a network blip on your end can cause the sender to retry a call that actually succeeded. Track processed job_id and event_id pairs and treat a repeat as a no-op. This is what separates a system that occasionally double-charges a customer or double-writes a record from one that doesn't.
Retry and backoff behavior. On the sender side, expect (and if you're building your own sender, implement) exponential backoff with jitter across a bounded retry window — this mirrors the model Stripe uses for webhook retries, now something of an industry default. On the receiver side, your endpoint should return a 2xx quickly and do the actual processing asynchronously, so a slow handler doesn't get mistaken for a failed delivery and trigger unnecessary retries. This is grounded in the same idea on both ends: transient failures are normal, and the system should absorb them without losing events, a pattern detailed in this retry and idempotency reference.
Dead-letter fallback. After retries are exhausted, failed deliveries shouldn't just vanish — route them to a dead-letter queue or log so you can reconcile manually or trigger a fallback poll.
The Hybrid Pattern: Webhook Primary, Poll as Fallback
Most teams running meaningful volume converge on a hybrid: register a webhook for the common case, but also poll the status endpoint as a periodic safety net for any job_id that hasn't confirmed within a threshold — say, 2x your expected p95 render time. This catches the rare case where your endpoint had downtime, a network partition swallowed the delivery, or a job silently stalled.
The extra complexity is worth it once job volume or business criticality rises enough that a missed webhook has real cost — a failed invoice PDF, a broken screenshot in a customer-facing report. For lower-stakes or lower-volume use, plain webhooks (or plain polling) are simpler and sufficient. Teams building larger notification pipelines on top of this pattern, such as scheduled monitoring jobs, benefit from the same reconciliation logic — see this three-signal monitoring pipeline architecture for a worked example.
Implementing This With Browsevra
Creating an async job through Browsevra's async job API works the same way whether you're generating a screenshot or a PDF: submit the render request, receive a job_id, and optionally register a webhook callback URL to be notified on completion. Payloads are signed so you can run HMAC verification against the raw body before trusting the result. The same API key that authenticates your synchronous screenshot calls also authenticates async job creation and webhook registration — no separate credential to provision. Treat that signing secret with the same care you'd give any API key; see this guide to API key management for practical guardrails.
To get the exact request shapes, retry timing, and signature header format, head to the Browsevra docs, and check pricing if you're scaling job volume and want to understand how async jobs are billed relative to sync calls.
Frequently Asked Questions
When does an async job make sense instead of a synchronous request?
Async makes sense whenever render time is unpredictable or can exceed a few seconds — heavy SPAs, long multi-page PDFs, or any workload where you can't guarantee a fast response. Synchronous calls work fine for simple, fast static-page screenshots where you can afford to hold a connection open for the full render.
What's the core tradeoff between webhooks and polling for rendering jobs?
Webhooks give near-instant notification with no wasted requests but require a public, reliable receiving endpoint. Polling needs no inbound infrastructure but adds latency tied to your poll interval and can waste rate-limit budget checking jobs that aren't done yet, especially with unpredictable render durations.
How should a webhook payload be authenticated?
Every payload should carry an HMAC-SHA256 signature computed over the raw request body, which your receiver recomputes and compares using constant-time comparison. Verifying against the raw body — not a re-parsed object — prevents subtle mismatches, and this check confirms the request genuinely came from Browsevra rather than a spoofed source.
What retry behavior should sender and receiver implement?
The sender should retry failed deliveries with exponential backoff and jitter across a bounded window, similar to Stripe's webhook retry model. The receiver should acknowledge with a fast 2xx response and process the payload asynchronously, so slow handling isn't mistaken for a failed delivery and doesn't trigger avoidable retries.
How do you avoid double-processing a webhook that fires twice?
Track a unique job_id and event_id for each delivery and check that pair before processing. Since delivery is at-least-once, treating a repeat delivery as a no-op is the standard idempotent handling pattern that prevents duplicate side effects like double writes or double notifications.
Switching from synchronous to async plus webhooks is mostly a configuration change: the same API key you already use for sync calls works for job creation and callback registration. Start with the Browsevra docs to wire up async job creation and a webhook callback URL, or explore browsevra directly to see how the pieces fit together.