Screenshot API or your own headless browser: the real cost
The code is 8 lines. The cost is everything around it. Put your own numbers into the calculator, see the work that hides under the waterline, and decide with a checklist that says when running it yourself is the better call.
a month = servers × server price + maintenance hours × hourly cost
first year = build hours × hourly cost + 12 × a month
What does running your own headless browser cost per month?
A self-hosted screenshot service costs servers plus people, and the people are usually the larger part. Every input below is yours to change. Only the server price is a published figure[7]; the rest are round placeholders, not industry averages.
Running it yourself
$224a month, after it is built
- Infrastructure
- $24 (1 server)
- People
- $200 (4 h)
- Build cost, once
- $2,000
- First year, self-hosted
- $4,688
- First year, API
- $948
The API side
Growth covers 10,000 screenshots a month: $79 a month, up to 15,000 screenshots, 150 requests a minute, 15 at once. All plans
Break-even
With these numbers self-hosting never catches up: it costs $145 more every month, on top of $2,000 to build.
- Self-hosted, 12 months: $4,688
- API, 12 months: $948
The formulae, in plain text
- 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.
Worked example with the defaults
- Renders one server finishes in a month: 2 × 2,592,000 ÷ 5 = 1,036,800.
- Servers needed: 10,000 × 4 ÷ 1,036,800 = 0.039, rounded up to 1.
- Infrastructure a month: 1 × $24 = $24.
- People a month: 4 hours × $50 = $200.
- Self-hosting a month: $24 + $200 = $224.
- Build cost, once: 40 hours × $50 = $2,000.
- First year, self-hosted: $2,000 + 12 × $224 = $4,688.
What the example shows
Where each default comes from
Show the table
| Input | Default | Where the default comes from |
|---|---|---|
| Screenshots a month | 10,000 | Example only Your number here, example only |
| Average seconds per render | 5 | Example only Your number here, example only |
| Peak compared with average | 4 | Example only Your number here, example only |
| Server price a month | $24 | Published price 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 | Example only Your number here, example only |
| Engineering hours to build it | 40 | Example only Your number here, example only |
| Hours a month to maintain it | 4 | Example only Your number here, example only |
| Cost of one engineering hour | $50 | Example only Your number here, example only |
The minimal Playwright script, and what it leaves out
These 8 lines of Node.js take a full-page screenshot with Playwright, and they do work. The script follows the first script in Playwright's library guide[1], with Chromium as the browser and the fullPage option from its screenshots guide[2]. It ran as written against Playwright 1.63 on 11 October 2026.
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();
})();Install first: npm i -D playwright, then npx playwright install chromium[1]. Puppeteer's version has the same shape: launch, new page, goto, screenshot, 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 gives you complete control at a mostly fixed cost; a screenshot API gives you less control at a cost that follows use. Neither column wins every row.
| 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
A self-hosted screenshot service is five components you run: a queue, an address check, browser workers, storage and monitoring. With an API the same components exist, on the provider's side of one HTTPS request.
Your own headless browser
A screenshot API
Is it safe to screenshot URLs that users submit?
A screenshot service that accepts addresses from users is a server that opens URLs someone else chose. Two protections make that safe, and both are your job when the browser is yours.
Server-side request forgery
In a server-side request forgery (SSRF) attack, the attacker abuses a feature of the server to read or change internal resources by choosing the URL the server fetches[6]. A headless browser inside your network is exactly such a feature. OWASP lists what it can be made to read:
- Cloud metadata, served at
http://169.254.169.254/, which can hold configuration and sometimes credentials - HTTP interfaces of internal databases
- Internal REST interfaces
- Local files, through
file://addresses
The defence is to resolve and check every address before the browser opens it, to repeat the check on every redirect and every request the page makes, and to refuse anything that is not public.
The Chromium sandbox
Chromium's sandbox limits what a page can do to the machine if it gets code running inside the browser. Its design document applies least privilege and assumes that sandboxed code is malicious[5].
Running without a sandbox is strongly discouraged.
The same guide allows --no-sandbox only "if you absolutely trust the content you open". 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]. A screenshot service opens pages it does not control, so the shortcut is not available to it.
What an API changes, and what it does not
When should you run it yourself, and when should you use an API?
Run your own headless browser when control, private pages or steady high volume matter most; use an API when time and upkeep do. Count the lines that are true for you.
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.
Many teams end up with both: their own browser for tests and pages inside the network, an API for screenshots of public pages in the product.
What one screenshot API request looks like
With CurlShot, a full-page screenshot of a public page without its cookie banner is one request. These are the operating facts its documentation states, nothing more.
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
- With cache=true a repeat of the same request is served from a stored copy, and a cache hit is not counted either. 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
- One bulk call starts up to 100 screenshots, each as its own job. Bulk screenshots
- Every plan has three limits: a monthly quota, requests per minute and renders at the same moment. The three 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
Glossary: headless browser, SSRF and the rest
- 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 people ask
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. Checked .
- [2]Playwright, Screenshots. Checked .
- [3]Playwright, Docker. Checked .
- [4]Puppeteer, Troubleshooting: Chrome sandbox, Docker and fonts. Checked .
- [5]The Chromium Projects, Sandbox design document. Checked .
- [6]OWASP Foundation, Server Side Request Forgery. Checked .
- [7]DigitalOcean, Droplet pricing. Checked .
Read next
- ExplainerScreenshots for AI agents: let a model see a pageWhen 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.
- GuideVisual regression testing, explained in picturesHow 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.
- Field guideWhy website screenshots look wrong, and the fixesA 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.