# Screenshot API vs headless browser: the real cost

> A cost calculator with every assumption editable, the hidden work behind headless Chrome, and an honest checklist for build or buy.

Calculator by Serghei Pogor, founder of CurlShot (https://curlshot.com/about/serghei-pogor). Published 20 October 2026. Page for people: https://curlshot.com/blog/screenshot-api-vs-headless-browser

Taking a screenshot with Playwright or Puppeteer takes 8 lines of code; running that as a service costs servers plus people. The monthly cost is servers × server price + maintenance hours × hourly cost, and the build hours come on top once. In this page's worked example (10,000 screenshots a month, one $24 server, 4 maintenance hours at an example $50) that is $224 a month and $4,688 in the first year, of which only $288 is servers. Run your own when you already have the infrastructure, need pages behind your firewall, need full scripted interaction, have high steady volume or strict data-residency rules. Otherwise compare the price of an API plan with your people cost, which is usually the larger number.

## Key facts

- The minimal Playwright script is 8 lines: launch, open a page, go to the address, screenshot, close. It handles none of the 11 jobs a production service needs.
- Self-hosting a month = servers × server price + maintenance hours × hourly cost. First year = build hours × hourly cost + 12 × that.
- In the worked example people are 89% of the monthly cost ($200 of $224); the server is $24. The engineering hours decide the answer, not the hardware.
- A server that renders 2 pages at once at 5 seconds each can finish 1,036,800 renders in a 30-day month if it never idles. Peaks, not the monthly total, decide how many servers you need.
- OWASP lists the cloud metadata address http://169.254.169.254/ and file:// addresses among the things a server can be made to read when it fetches a user-supplied URL.
- Puppeteer’s documentation calls running Chrome without its sandbox "strongly discouraged"; a browser that opens untrusted pages must keep it on.

## What does running your own headless browser cost per month?

The cost of a self-hosted screenshot service is servers plus people. The interactive calculator on the page lets you change every input; the formulae and the default inputs are below, so you can do the same sum by hand.

### The formulae

- **Renders one server finishes in a month** = renders at once × 2,592,000 seconds ÷ seconds per render
- **Servers needed** = screenshots a month × peak factor ÷ renders one server finishes in a month, rounded up, at least 1
- **Infrastructure a month** = servers needed × server price a month
- **People a month** = maintenance hours a month × cost of one hour
- **Self-hosting a month** = infrastructure a month + people a month
- **Build cost, once** = hours to build × cost of one hour
- **First year, self-hosted** = build cost + 12 × self-hosting a month
- **First year, API** = 12 × price of the plan that covers your volume
- **Break-even, in months** = build cost ÷ (API price a month − self-hosting a month), only when the API costs more a month

A 30-day month has 30 × 24 × 3,600 = 2,592,000 seconds.

### The inputs and where each default comes from

Only the server price is a published figure. Every other default is a round placeholder, not an industry average: replace it with your own number.

| Input | Default | Where the default comes from |
| --- | --- | --- |
| Screenshots a month | 10,000 | Your number here, example only |
| Average seconds per render | 5 | Your number here, example only |
| Peak compared with average | 4 | Your number here, example only |
| Server price a month | $24 | DigitalOcean Basic Droplet, Regular CPU, 2 vCPUs and 4 GiB of memory: $24.00 a month on the public price list, read on 11 October 2026 [7] |
| Renders one server runs at once | 2 | Your number here, example only |
| Engineering hours to build it | 40 | Your number here, example only |
| Hours a month to maintain it | 4 | Your number here, example only |
| Cost of one engineering hour | $50 | Your number here, example only |

### Worked example with the defaults

1. Renders one server finishes in a month: 2 × 2,592,000 ÷ 5 = 1,036,800.
2. Servers needed: 10,000 × 4 ÷ 1,036,800 = 0.039, rounded up to 1.
3. Infrastructure a month: 1 × $24 = $24.
4. People a month: 4 hours × $50 = $200.
5. Self-hosting a month: $24 + $200 = $224.
6. Build cost, once: 40 hours × $50 = $2,000.
7. First year, self-hosted: $2,000 + 12 × $224 = $4,688.

In this example people are 89% of the monthly cost. For the API side, take the price of the plan whose monthly quota covers your volume from [the pricing page](https://curlshot.com/pricing): first year = 12 × that price, and break-even in months = build cost ÷ (API price a month − self-hosting a month). When self-hosting costs as much or more a month, it never breaks even.

## What is below the waterline when you run headless Chrome yourself?

Launching a browser and calling screenshot() is the visible 8 lines. A screenshot service that other people depend on also has to do these 11 jobs:

1. **Crashes, memory and recycling.** A browser is a heavy, long-running process. Workers need memory limits, restarts and cleanup of processes left behind. Playwright warns that Chromium in Docker can run out of memory and crash without the right flags. [3]
2. **Concurrency and queues.** One server renders only a few pages at the same moment. Requests beyond that have to wait in a queue, with a limit, an order and a way to report progress.
3. **Fonts, emoji and CJK text.** A bare Linux server has few fonts. Puppeteer’s own Docker recipe installs extra font packages for Chinese, Japanese, Arabic, Hebrew and Thai; without them those scripts render as empty boxes. [4]
4. **Lazy loading and waiting.** The load event fires before lazy images and client-rendered content arrive. You decide what "ready" means, and scroll the page so a full-page capture has no blank bands.
5. **Cookie banners and ads.** Consent dialogs, ads and chat widgets sit on top of the content until you block or hide them, and the rules that do it need upkeep.
6. **Timeouts and retries.** Some pages never finish loading. Every step needs a deadline, and a failed render needs a retry policy that does not hammer a broken page.
7. **SSRF protection.** When the address comes from a user, the browser can be pointed at your own network. Addresses, redirects and DNS answers all have to be checked. [6]
8. **Sandboxing untrusted pages.** Any page can run code in your browser. Chromium’s sandbox has to stay on, which rules out the common shortcut of running as root with --no-sandbox. [4]
9. **Keeping Chromium updated.** Browser security fixes ship regularly. Each update can change rendering, so it needs a test pass before it reaches production.
10. **Caching and storage.** Screenshots are files: they need a place to live, an expiry, and a cache so the same page is not rendered a thousand times.
11. **Monitoring.** Queue depth, render time, failure rate and memory per worker, with an alert that reaches a person before the customers notice.

## The minimal Playwright screenshot script, and what it leaves out

This 8-line Node.js script takes a full-page screenshot of a page with Playwright. It follows the first script in Playwright's library guide [1] with Chromium as the browser and the fullPage option from the screenshots guide [2], and it ran as written against Playwright 1.63 on 11 October 2026. Install with `npm i -D playwright` and `npx playwright install chromium`.

```js
const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({ path: 'example.png', fullPage: true });
  await browser.close();
})();
```

What those lines do not handle:

- **A page that never finishes loading.** It relies on the library’s default timeouts. When one fires the script throws: nothing retries, and the browser is left running.
- **Two requests at once.** It launches a whole browser per run. Ten callers means ten browsers, with nothing to limit or queue them.
- **Lazy-loaded images.** fullPage captures the full scrollable page, but nothing scrolls it first, so content that loads on scroll can be missing. [2]
- **Cookie banners, ads and chat widgets.** Whatever the page shows a first-time visitor is in the image.
- **Missing fonts on the server.** It renders with the fonts the machine has. A minimal Linux image lacks many scripts and emoji. [4]
- **An address chosen by someone else.** page.goto opens whatever it is given, including addresses inside your network. [6]
- **A crash.** If the browser dies, the promise rejects and browser.close() never runs. Nothing restarts it or cleans up.
- **Where the file goes.** It writes example.png to the local disk. No storage, no expiry, no cache.

## Headless browser vs screenshot API, side by side

|  | Your own headless browser | A screenshot API |
| --- | --- | --- |
| **Control** | Complete. Any browser flag, any script, any sequence of clicks and typing. | The options the API offers. Usually wide for capture, narrow for multi-step interaction. |
| **Time to first screenshot** | Minutes on your laptop (8 lines). A production service takes the build hours you enter in the calculator. | One HTTP request after you have a key. |
| **Ongoing work** | Yours: browser updates, fonts, blocked pages, capacity, alerts. | The provider’s. You keep an eye on usage and handle error responses. |
| **Scaling** | You add servers and tune the queue. You pay for peak capacity even when idle. | Bounded by the plan: a monthly quota, a rate limit and a concurrency limit. |
| **Pages behind a login or firewall** | Anything your network reaches, including intranet hosts and localhost. | Public pages, and signed-in pages when you send a cookie or header. Private addresses are out of reach. |
| **Security surface** | You run a browser that opens arbitrary pages inside your infrastructure: SSRF checks, sandbox and patching are yours. | The browser runs outside your network. You send page addresses, and any cookies you pass, to a third party. |
| **Cost shape** | Mostly fixed: servers plus people, almost flat until you need another server. | Per use: a plan sized by screenshots a month, stepping up with volume. |
| **Failure handling** | You write it: timeouts, retries, crash recovery, dead jobs. | An error code per failure. You still decide whether to retry. |

## Two architectures: five moving parts or one HTTP call

- **Self-hosted:** your app puts a job on a queue; an address check (SSRF guard) approves it; browser workers take jobs from the queue and render; the image goes to storage; monitoring watches the queue, the workers and the storage. You run all five.

- **API:** your app sends one HTTPS request with the page address and receives the image in the response. The queue, the workers, the storage and the monitoring exist too, on the provider’s side.

## Is it safe to screenshot URLs that users submit?

A screenshot service that accepts addresses from users is a server that fetches attacker-chosen URLs, which is the definition of a server-side request forgery (SSRF) risk. OWASP describes what such a server can be made to read: cloud metadata served at http://169.254.169.254/, internal HTTP interfaces of databases, internal REST interfaces, and local files through file:// addresses [6]. The defence is to resolve and check every address before the browser opens it, and again on every redirect and sub-request, and to refuse anything that is not public.

Chromium's sandbox limits what a page can do if it manages to run malicious code in the browser. Chromium's design document treats sandboxed code as malicious by assumption and applies least privilege to it [5]. Puppeteer's documentation says Chrome can be launched with --no-sandbox only "if you absolutely trust the content you open", and states: "Running without a sandbox is strongly discouraged." [4]. Playwright's Docker guide notes that the default root user disables the Chromium sandbox, and recommends a separate user and a seccomp profile for crawling untrusted sites [3].

An API moves the browser out of your network, which removes this class of risk from your infrastructure. It does not remove it from the world: you are trusting the provider to do the same work, and you are sending it the addresses and any cookies you pass.

## When should you run it yourself, and when should you use an API?

### Run it yourself if

- You already run browser automation in production, with the people and the monitoring for it.
- The pages are behind your firewall: intranet tools, localhost, staging hosts with no public address.
- You need full scripted interaction: multi-step flows, typing, file uploads, custom browser flags.
- Your volume is high and steady, so servers stay busy and the fixed cost is spread over many renders.
- Page content or addresses may not leave your infrastructure or a given region.
- Screenshots are a by-product of tests you already run in a browser, such as visual regression checks.

### Use an API if

- Screenshots are a feature of your product, not the product, and nobody wants to own a browser fleet.
- Volume is low, spiky or unknown, so a server would sit idle most of the month.
- The addresses come from your users, and you would rather not run the browser that opens them inside your network.
- You need it this week: one HTTP request instead of a queue, workers and storage.
- The pages are public, or reachable with a cookie, a header or basic authentication.
- Maintenance hours are worth more to you spent on your own product.

## What one screenshot API request looks like

With CurlShot, a full-page screenshot without the cookie banner is one request:

```bash
curl "https://curlshot.com/api/v1/screenshot?access_key=YOUR_ACCESS_KEY&url=https://example.com&full_page=true&block_cookie_banners=true" \
  --output example.png
```

- One successful render uses one screenshot from the monthly quota, whatever its size. A request that fails with any error is not counted. ([Usage and limits](https://curlshot.com/docs/usage-and-limits))
- With cache=true a repeat of the same request is served from a stored copy, and a cache hit is not counted either. ([Caching](https://curlshot.com/docs/caching))
- With async=true the API answers at once with a job; you poll the job or receive a webhook when it is done. ([Async and webhooks](https://curlshot.com/docs/async-and-webhooks))
- One bulk call starts up to 100 screenshots, each as its own job. ([Bulk screenshots](https://curlshot.com/docs/bulk))
- Every plan has three limits: a monthly quota, requests per minute and renders at the same moment. ([The three limits](https://curlshot.com/docs/usage-and-limits))
- Signed-in pages work by sending cookies, headers or an authorization value. localhost and private network addresses are refused, so pages behind your firewall need your own browser. ([Screenshot behind a login](https://curlshot.com/tools/screenshot-behind-login))

More: [the screenshot API](https://curlshot.com/tools/screenshot-api), [getting started](https://curlshot.com/docs/getting-started), [plans and prices](https://curlshot.com/pricing).

## Glossary

- **Headless browser:** A real web browser running without a visible window, driven by a program instead of a person. It loads and renders pages exactly as a normal browser does.
- **Puppeteer:** A JavaScript library, maintained by the Chrome team, that controls Chrome or Firefox from Node.js.
- **Playwright:** A browser automation library from Microsoft that controls Chromium, Firefox and WebKit, with bindings for JavaScript, Python, Java and .NET.
- **SSRF:** Server-side request forgery: an attack that makes a server send requests to an address the attacker chooses, typically to reach internal services the attacker cannot reach directly.
- **Concurrency:** How many renders run at the same moment. It is limited by the memory and CPU of the machine, and it sets how much traffic one server can take at a peak.
- **Cold start:** The delay while a browser process starts before it can open its first page. Services avoid it by keeping browsers running, which costs memory while they wait.
- **Queue:** A list of render jobs waiting for a free browser. It turns a burst of requests into a steady flow the workers can handle.

## Questions

### Is it cheaper to run Puppeteer or Playwright myself than to use a screenshot API?

Only when your engineering time is already paid for or your volume is high and steady. The servers are the small part: in the worked example on this page the server is $24 a month, and 4 hours of maintenance at an example $50 an hour is $200. Put your own hours and rates in the calculator; the answer follows the people cost.

### How many screenshots can one server take?

Renders at once × 2,592,000 seconds in a 30-day month ÷ seconds per render. A server that runs 2 renders at once at 5 seconds each can finish 1,036,800 a month if it is never idle. Real traffic has peaks, so divide by how much busier your busiest moment is than the average. Both inputs are yours to measure; neither is an industry figure.

### When is running my own headless browser the better choice?

When you already operate browser automation, when the pages are behind your firewall or on localhost, when you need multi-step scripted interaction, when volume is high and steady enough to keep servers busy, or when page content may not leave your infrastructure.

### Why is rendering user-supplied URLs a security risk?

Because the browser runs inside your network and opens whatever address it is given. That is server-side request forgery: OWASP describes attackers using it to read cloud metadata at http://169.254.169.254/, internal HTTP interfaces and local files through file:// addresses. Every address, redirect and DNS answer has to be checked before the browser opens it.

### Can I run headless Chrome with --no-sandbox?

Only for content you fully trust. Puppeteer’s documentation says running without a sandbox is strongly discouraged, and Playwright’s Docker guide recommends a separate non-root user and a seccomp profile for crawling untrusted sites. A screenshot service opens pages it does not control, so the sandbox should stay on.

### Does CurlShot count failed screenshots?

No. On CurlShot one successful render uses one screenshot from the monthly quota. A request that fails with any error is not counted, and neither is a cache hit when caching is turned on for the request.

## Sources

1. Playwright, "Library: first script": https://playwright.dev/docs/library (checked 11 October 2026)
2. Playwright, "Screenshots": https://playwright.dev/docs/screenshots (checked 11 October 2026)
3. Playwright, "Docker": https://playwright.dev/docs/docker (checked 11 October 2026)
4. Puppeteer, "Troubleshooting: Chrome sandbox, Docker and fonts": https://pptr.dev/troubleshooting (checked 11 October 2026)
5. The Chromium Projects, "Sandbox design document": https://chromium.googlesource.com/chromium/src/+/main/docs/design/sandbox.md (checked 11 October 2026)
6. OWASP Foundation, "Server Side Request Forgery": https://community.owasp.org/attacks/Server_Side_Request_Forgery (checked 11 October 2026)
7. DigitalOcean, "Droplet pricing": https://www.digitalocean.com/pricing/droplets (checked 11 October 2026)

## Go deeper

- [Screenshot API: one HTTP request](https://curlshot.com/tools/screenshot-api)
- [Puppeteer and Playwright calls as API options](https://curlshot.com/tools/puppeteer-screenshot-api)
- [Getting started with the API](https://curlshot.com/docs/getting-started)
- [How usage is counted](https://curlshot.com/docs/usage-and-limits)
- [Plans and prices](https://curlshot.com/pricing)

## Read next

- [Screenshots for AI agents: let a model see a page](https://curlshot.com/blog/screenshots-for-ai-agents.md): When to give an AI agent a screenshot, HTML, markdown or the accessibility tree, what a screenshot costs in tokens on each vendor, and how to wire it up.
- [Visual regression testing, explained in pictures](https://curlshot.com/blog/visual-regression-testing-guide.md): How screenshot comparison works, why visual tests go flaky and how to fix each cause, the tools, and a set-up checklist with a Playwright example.
- [Why website screenshots look wrong, and the fixes](https://curlshot.com/blog/why-website-screenshots-look-wrong.md): A field guide to 11 ways a website screenshot goes wrong, from blank lazy images to cookie banners and cut-off pages, with the cause and the fix for each.
