Compared
Browserless alternative for PDFs and screenshots
Browserless rents you a browser. RenderBadger hands you the finished file. If a render is all you need, that difference is the whole comparison, and Browserless is cheaper only until you count the code you have to keep.
Who this page is for
You wired Puppeteer to Browserless because your app needed an invoice PDF or a screenshot, and it worked. Months later you maintain a browser-session wrapper, a retry loop, a timeout, a place to put the output and a way to hand it to the customer, for what is really one function call. Or a unit bill arrived that was hard to connect to the number of documents you made.
If you use Browserless for scraping, testing, form automation or agents, this page is not for you. Keep it. It is good at that, and RenderBadger does not do it at all.
What each one gives you
Browserless is a hosted Chrome. You connect with Puppeteer or Playwright, drive the page yourself, call page.pdf(), and get bytes back over the socket. Storage, links, retries, idempotency and usage records are your code.
RenderBadger is a rendering API. You POST a URL or HTML with the options you would have passed to page.pdf(), and get the file back, or a signed URL you can hand to a browser or an email. The browser is behind the endpoint and you never touch it.
| Browserless | RenderBadger | |
|---|---|---|
| What you get | A browser session | A PDF or image file |
| Code you maintain | Puppeteer or Playwright scripts, session handling, retries | One HTTP request |
| PDF options | Whatever you pass to page.pdf() | The same options as request fields: format, margins, header and footer templates, page ranges, print media |
| Screenshots | page.screenshot() | Viewport, full page, element, device scale, PNG, JPEG, WebP |
| Output storage | Yours | Included, with signed URLs and retention by plan |
| Idempotency and job records | Yours | Idempotency keys, job status, duration and size per render |
| Async and webhooks | Yours | Queue a render, get a signed POST when it finishes |
| Billing unit | 30 seconds of browser time; a reconnect is a new unit | One render, whatever it takes |
| Free tier | 1,000 units, 2 browsers | 100 renders, 1 concurrent |
| Scraping, forms, captchas, proxies | Yes | No, by design |
| Session recording and debugging | Yes | No |
| Self-hosting | Licensed | No |
Pricing at 2,000 and 10,000 renders
Browserless bills in units: one unit is up to 30 seconds of browser time on one connection. A typical invoice renders in three to five seconds, so one render is one unit unless your code reconnects or waits. Prices below are the ones Browserless lists for annual billing; monthly billing costs more and is not published.
| Renders a month | Browserless | RenderBadger |
|---|---|---|
| 2,000 | Prototyping, $25, with 20,000 units | Starter, $19 |
| 10,000 | Prototyping, $25, same plan | Pro, $49 |
On that table Browserless wins at 10,000 renders, and it would be dishonest to hide it. What the $25 does not include is the wrapper you write and run: connecting, isolating a context per render, waiting for fonts, catching the hung page, retrying the failed one, uploading the output somewhere, signing a link, recording what happened. That is a few hundred lines on day one and a pager on month-end nights. The $24 difference buys you not owning them. If you are happy owning them, Browserless is the better deal.
Prices checked October 26, 2026. Both in US dollars.
Where Browserless is the better choice
Anything beyond a plain render. Scraping a site that fights back, filling forms, driving a flow for a test suite, running an agent, solving captchas, routing through proxies, recording a session to see what went wrong. Browserless has all of it. RenderBadger has none of it, and will not. The same goes for volume: above fifty thousand renders a month, or when you want to self-host, Browserless is the right shape.
Switching
The options keep their Puppeteer names on purpose. What you pass to page.pdf() goes into the request body as it is. Here is a render that is typical of what we see, on both sides.
import puppeteer from "puppeteer-core";
const browser = await puppeteer.connect({
browserWSEndpoint: `wss://production-sfo.browserless.io?token=${TOKEN}`,
});
try {
const context = await browser.createBrowserContext();
const page = await context.newPage();
page.setDefaultTimeout(30_000);
await page.setContent(html, { waitUntil: "networkidle0" });
await page.evaluateHandle("document.fonts.ready");
const pdf = await page.pdf({
format: "A4",
printBackground: true,
margin: { top: "20mm", bottom: "20mm" },
displayHeaderFooter: true,
footerTemplate: footer,
});
await context.close();
const key = `invoices/${invoiceId}.pdf`;
await s3.putObject({ Bucket, Key: key, Body: pdf });
const url = await getSignedUrl(s3, new GetObjectCommand({ Bucket, Key: key }));
// retries, timeouts, and the failed-render log are still to write
} finally {
await browser.disconnect();
}
const res = await fetch("https://renderbadger.sh/v1/pdf", {
method: "POST",
headers: { authorization: `Bearer ${KEY}`, "content-type": "application/json" },
body: JSON.stringify({
html,
format: "A4",
margin: { top: "20mm", bottom: "20mm" },
footerTemplate: footer,
output: "url",
}),
});
const job = await res.json(); // { id, status: "done", url, expiresAt, durationMs }
Three things to know when you move:
- Fonts are waited for before printing, so there is no
document.fonts.readyto call. - A render that takes longer than 30 seconds should be sent with
async: true; you get a job id, and a webhook orGET /v1/jobs/:idwhen it is done. Async needs the Starter plan. - Send an
Idempotency-Keyheader from your own invoice id. A retry after a network error then returns the same render instead of making and billing a second one.
The full option list is in the docs.
Try it on your own invoice
The free plan is 100 renders a month, no card, and the key is live the moment you create it.