Gotenberg and Browserless both put headless Chromium behind an HTTP port in a Docker container, so they get compared a lot. They are built for different jobs.
- Gotenberg is a document conversion API. You POST a form with a URL, HTML or an Office file, and you get a PDF or image back. It bundles LibreOffice and PDF tools (merge, split, PDF/A).
- Browserless is a browser service. It has REST routes for screenshots, PDFs and scraping, but its main job is to host Chrome sessions that your own Puppeteer or Playwright code drives over a WebSocket.
If you only need "HTML in, PDF out", Gotenberg is simpler, faster in our test, and MIT licensed. If you need to log in, click, scroll or run custom scripts before the capture, Browserless is the one that can do it.
Disclosure: I build RenderScreenshot, a hosted screenshot and PDF API. We ran both containers on our own machine on 2026-10-15. We did not test Browserless's hosted cloud.
For other pairings see the comparison hub. If you are deciding between a self-hosted container and a hosted browser, Browserless vs Cloudflare Browser Rendering covers that side.
Verdict
| Your requirement | Gotenberg | Browserless |
|---|---|---|
| HTML or URL to PDF | Yes, form fields | Yes, JSON body |
| URL screenshot (PNG, JPEG, WebP) | Yes | Yes |
| Word, Excel, PowerPoint to PDF | Yes, LibreOffice built in | No |
| Merge, split, PDF/A, PDF/UA | Yes | No |
| Run your own Puppeteer or Playwright script | No | Yes, over WebSocket |
| Log in, click, fill forms before capture | Limited to injected waits | Yes |
| Licence for a closed-source commercial product | MIT | SSPL or paid commercial licence |
| Where it runs | Your container | Your container, or the browserless.io cloud |
Test setup
Method: 2026-10-15, macOS on an Apple M1 Pro, Docker through OrbStack with 10 CPUs and 16 GB available to the VM. Images: gotenberg/gotenberg:8 (8.37.0, Chromium 152.0.7977.82) and ghcr.io/browserless/chromium:latest (Browserless 2.57.0, Chrome 156.0.8078.4), both with default settings except TOKEN and CONCURRENT=2 for Browserless. Test pages came from a static server on the host, reached as host.docker.internal. Five sequential runs per case from a Node 22 client. "First" is the first request after the container was ready; "median" covers all five.
| Case | Gotenberg first / median | Browserless first / median | Output (Gotenberg / Browserless) |
|---|---|---|---|
| 3-page invoice, A4 PDF | 806 / 90 ms | 341 / 209 ms | 27 KB / 109 KB |
| JavaScript page, A4 PDF | 77 / 75 ms | 203 / 196 ms | 18 KB / 19 KB |
| example.com, A4 PDF | 269 / 129 ms | 347 / 347 ms | 92 KB / 122 KB |
| 1200x630 card, PNG | 208 / 208 ms | 361 / 337 ms | 361 KB / 365 KB |
| Long page with lazy images, PNG | 178 / 187 ms | 352 / 333 ms | 85 KB / 85 KB, both 1280x8657 |
Every request succeeded: 50 of 50. Both services printed the @page counter "Page 1 of 3" on the invoice.
| Container facts | Gotenberg | Browserless |
|---|---|---|
| Image size reported by Docker (arm64) | 2.43 GB | 3.64 GB |
Time from docker run to a healthy response |
0.6 s | 2.4 s |
| Memory after the test, idle | 224 MiB | 300 MiB |
| What else is inside | LibreOffice, PDF tools | Chrome plus the Browserless platform (debugger, session APIs) |
Gotenberg's invoice PDF was four times smaller than the Browserless one from a newer Chrome on the same HTML. We did not dig into why. If PDF size matters to you (email attachments, storage), test your own templates; the difference may come from the Chrome version or from processing after print.
Neither waited for late JavaScript
Our JavaScript test page changes a line of text 800 ms after load. Both services missed it with default settings. Both have a fix, and both fixes worked:
#!/bin/sh # Wait for the late JS update before printing, on both services. set -eu COND="document.querySelector('#late').textContent.startsWith('JS-LATE-OK')" curl -sf -o out/gotenberg-js-waited.pdf \ --form url=http://host.docker.internal:8909/js.html \ --form "waitForExpression=$COND" \ http://localhost:3010/forms/chromium/convert/url curl -sf -o out/browserless-js-waited.pdf \ -H 'Content-Type: application/json' \ -d "{\"url\":\"http://host.docker.internal:8909/js.html\",\"waitForFunction\":{\"fn\":\"() => $COND\",\"timeout\":5000}}" \ "http://localhost:3011/pdf?token=${BROWSERLESS_TOKEN:-b09-local-token}" for f in out/gotenberg-js-waited.pdf out/browserless-js-waited.pdf; do printf '%s: ' "$f"; pdftotext "$f" - | grep -o -E 'JS-LATE-OK|LATE-PENDING' done
Both files then contained JS-LATE-OK. Gotenberg's field is waitForExpression; Browserless uses waitForFunction with a stringified function. A fixed delay (waitDelay in Gotenberg, waitForTimeout in Browserless) also works, but it wastes time on fast pages and fails on slow ones.
Gotenberg screenshots are full page by default
We sent Gotenberg a 1280x800 screenshot request for a long page without clip. It returned a 1280x8657 image: the whole page. Its docs list clip with a default of false, and clip: true limits the image to the viewport. Browserless follows Puppeteer and captures the viewport unless you pass fullPage: true. If you move code from one to the other, check this first.
Run both in five minutes
docker run -d --name gotenberg -p 3010:3000 gotenberg/gotenberg:8 docker run -d --name browserless -p 3011:3000 -e TOKEN=change-me -e CONCURRENT=2 \ ghcr.io/browserless/chromium # Gotenberg: multipart form, PDF back (US Letter unless you send paperWidth/paperHeight) curl -sf -o gotenberg.pdf --form url=https://example.com \ http://localhost:3010/forms/chromium/convert/url # Browserless: JSON body, token in the query string, PDF back curl -sf -o browserless.pdf -H 'Content-Type: application/json' \ -d '{"url":"https://example.com","options":{"format":"A4"}}' \ "http://localhost:3011/pdf?token=change-me"
We ran these two requests on 2026-10-15: Gotenberg returned a Letter-size PDF and Browserless an A4 PDF. Pin the image versions in production. Both projects release often, and a new Chromium can change layout or fonts.
Driving Browserless with your own Puppeteer code
This is the capability Gotenberg does not have. Connect Puppeteer to the container and use the full page API:
// Drive a self-hosted Browserless container with your own Puppeteer code. // docker run -p 3000:3000 -e TOKEN=change-me ghcr.io/browserless/chromium import puppeteer from 'puppeteer-core'; const endpoint = `ws://localhost:3000?token=${process.env.BROWSERLESS_TOKEN}`; const browser = await puppeteer.connect({ browserWSEndpoint: endpoint }); try { const page = await browser.newPage(); await page.setViewport({ width: 1280, height: 800 }); await page.goto('https://example.com', { waitUntil: 'networkidle0' }); await page.screenshot({ path: 'example-browserless.png' }); } finally { await browser.close(); // ends the remote session and frees the slot }
We ran it with Puppeteer 24.43.1 against the container on port 3011. It returned a 1280x800 PNG.
Option map
| Task | Gotenberg 8 (form field) | Browserless 2 (JSON) |
|---|---|---|
| PDF from URL | POST /forms/chromium/convert/url, url |
POST /pdf, url |
| PDF from HTML | POST /forms/chromium/convert/html, file index.html |
POST /pdf, html |
| Screenshot | POST /forms/chromium/screenshot/url |
POST /screenshot |
| Paper size | paperWidth, paperHeight (inches by default) |
options.format or options.width/height |
| Backgrounds | printBackground |
options.printBackground |
| Viewport | width, height (screenshot routes) |
viewport object |
| Wait for a condition | waitForExpression |
waitForFunction, waitForSelector |
| Fixed wait | waitDelay (for example 500ms) |
waitForTimeout |
| Navigation rule | skipNetworkIdleEvent |
gotoOptions.waitUntil |
| Block requests | Outbound URL filtering settings | rejectResourceTypes, rejectRequestPattern |
| Auth to the service | None by default; add your own proxy | TOKEN env var, ?token= on each request |
Gotenberg has no authentication by default. Do not expose its port to the internet. Put it on a private network or behind a proxy that checks a key.
Costs beyond the licence
Self-hosting is not free once you count the work:
- Patching. Chromium ships security fixes every few weeks. You rebuild and redeploy to get them.
- Fonts. Check which fonts each image ships before you trust its output. Brand fonts, and any CJK or emoji coverage the image lacks, must be added to your image or loaded by the page with
@font-face. - Capacity. Each Chrome page uses real memory. Set Browserless
CONCURRENTand queue limits, and give Gotenberg enough replicas, before a traffic spike finds the limit for you. - SSRF. Both services fetch any URL you send. If users supply URLs, filter them before they reach the container.
If you would rather not run Chrome at all, Browserless sells a hosted plan, priced in units of up to 30 seconds of browser time (its pricing page lists a free tier of 1,000 units a month, checked 2026-10-15).
When each one wins
Gotenberg wins for back-office document pipelines: invoices, reports, contracts, Office files to PDF, PDF/A archiving. One container covers HTML and Office input, and the MIT licence keeps lawyers out of it.
Browserless wins when a capture is the last step of a browser session: log in, accept a dialog, scroll a feed, then screenshot. It also suits teams that already have Puppeteer or Playwright code and want to move the browsers off their app servers.
A hosted API wins when the capture is simple but you do not want to own Chromium updates, fonts and scaling. RenderScreenshot is one; it renders with Chromium on Cloudflare, caches every result, and returns a public CDN link in X-Cache-URL. Its waits work like the fixes above: see the wait parameters for for_selector and until. We do not offer Office-to-PDF conversion or a raw WebSocket browser, so Gotenberg and Browserless each keep a use case we do not cover.
curl -sf -o example.pdf https://api.renderscreenshot.com/v1/screenshot \ -H "Authorization: Bearer $RS_API_KEY" -H 'Content-Type: application/json' \ -d '{"url":"https://example.com","output":{"format":"pdf"},"pdf":{"paper":"a4"},"wait":{"until":"networkidle"}}'
We checked this request against a local mock of our API, not the live service. Get a free API key and send the same five test pages through it next to your containers.
Last tested 2026-10-15 with gotenberg/gotenberg:8 (8.37.0), ghcr.io/browserless/chromium (2.57.0), Node 22.18.0 and Puppeteer 24.43.1 on macOS (Apple M1 Pro) with OrbStack.