Gotenberg vs Browserless: Self-Hosted PDF and Screenshots

Gotenberg vs Browserless run side by side in Docker: PDF and screenshot latency, idle memory, image size, licence terms and the waits each one needs.

By Simon Chiu · Updated

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 CONCURRENT and 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.

Frequently asked questions

Is Gotenberg or Browserless free for commercial use?
Gotenberg is MIT licensed. The Browserless image is dual-licensed under SSPL-1.0 or a Browserless commercial licence, and its licence file says closed-source commercial sites, applications and CI systems need the commercial licence. Read both licences yourself before you ship.
Can Gotenberg take screenshots?
Yes. Gotenberg 8 has screenshot routes for URLs, HTML and Markdown next to its PDF routes. In our test the URL screenshot route captured the full page by default; set clip to true to cut it to the viewport.
Can I run my own Puppeteer or Playwright code on Gotenberg?
No. Gotenberg exposes fixed HTTP routes with form fields. Browserless exposes REST routes and also accepts Puppeteer and Playwright connections over WebSocket, so you can run any script against it.
Which one is faster?
On our machine Gotenberg answered warm PDF requests in 75 to 130 ms and Browserless in about 200 to 350 ms for the same pages. Both were far below a cold Chrome launch. Your numbers will depend on CPU, concurrency and page weight.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.