Back to blog

Self-Hosted Screenshot and PDF Rendering API on a VPS in 2026

August 15, 2026Mario Marin
Last updated: July 2026
Every URL-to-screenshot or HTML-to-PDF SaaS bills per request once free tiers run out, and a documentation site, e-commerce catalog, or reporting dashboard that renders thousands of pages a month turns into a recurring line item that never shrinks. A self-hosted screenshot API VPS running Puppeteer or Playwright behind a small Node or Python service eliminates the per-request meter entirely — the only real planning question is how much RAM headless Chrome actually needs and how to stop one bad page from taking the whole box down.

Why developers move off Urlbox and Browserless

Hosted screenshot and PDF rendering APIs are convenient for a prototype, but the pricing model punishes exactly the workloads that succeed: cron-driven thumbnail regeneration, invoice PDF generation at checkout, social-card previews on every blog post, or a monitoring tool that screenshots dashboards every few minutes. Each of those is a fixed, predictable job — not a spiky, unpredictable one — which is precisely the case where owning the compute beats renting it by the call. Running Chromium yourself on a KVM VPS with full root access removes the request quota altogether; the only cost is the server itself.

How much RAM does headless Chrome actually need

The number that matters for sizing is per-instance memory, not total server RAM. As a working rule of thumb, budget roughly 300–500MB of RAM per concurrent headless Chrome instance — the exact figure moves with page complexity (heavy JavaScript, large images, long PDFs render higher; simple static pages render lower). Reserve a slice of the box for the OS, your API process, and Node/Python runtime overhead before dividing the rest by that per-instance figure. This is the single calculation that determines whether a Nano tier is fine for a side project or whether a bulk PDF-generation job needs a Pro or Business tier.

Sizing a Self-Hosted Screenshot API VPS by Chrome Concurrency

Using the 300–500MB-per-instance rule and setting aside roughly 1–2GB for the OS, the API process, and runtime overhead, here's a rough read on how X-Zone Servers' KVM VPS tiers translate to concurrent rendering capacity. Treat these as back-of-envelope planning numbers, not a tested benchmark — actual counts shift with page complexity and how heavily the OS reserve is estimated.
TierSpecsPriceRough concurrent Chrome capacity
Nano2GB / 1 vCPU / 40GBEUR4/moA couple of instances — personal projects, low-traffic sites
Micro4GB / 2 vCPU / 80GBEUR6/moSmall batch, roughly half a dozen
Starter8GB / 2 vCPU / 120GBEUR10/moLow teens — vCPU count starts limiting this tier before RAM does
Basic16GB / 4 vCPU / 160GBEUR16/moRoughly 25–35
Pro24GB / 6 vCPU / 200GBEUR30/moRoughly 40–55
Business32GB / 8 vCPU / 300GBEUR58/moRoughly 55–75
Note the Starter tier: it doubles the RAM of Micro but keeps the same 2 vCPU count, so at higher concurrency the CPU — not memory — throttles rendering throughput first. If the workload is bulk PDF generation or JavaScript-heavy page rendering rather than simple screenshots, weight the vCPU count more heavily than the RAM figure when picking a tier. If a rendering farm needs to push well past these ranges, X-Zone Servers' dedicated servers scale up to 256 cores and 1TB of RAM for that kind of volume.

Docker resource limits: the part most self-hosted setups skip

A single unbounded Chrome process can happily consume every byte of free RAM on a page with an infinite scroll bug, a memory-leaking canvas animation, or a PDF with thousands of embedded images — and take the entire VPS down with it, including your API process and any other services on the box. Running the rendering workload in Docker on a full-root KVM VPS with explicit resource limits is not optional hardening, it's the baseline for a production self-hosted screenshot API:
  • Set a hard memory ceiling per container (`--memory=512m` or the equivalent `mem_limit` / `deploy.resources.limits.memory` in Compose) sized to your per-instance RAM budget from the table above, not to the whole server's total.
  • Cap CPU shares per container (`--cpus=0.5` or `deploy.resources.limits.cpus`) so one render job can't starve the others queued behind it.
  • Launch Chromium with `--disable-dev-shm-usage` so it uses disk-backed temp storage instead of the container's small default `/dev/shm`, which otherwise crashes renders on large pages.
  • Set a render timeout in your application code and kill the browser process on timeout — a hung page should never be allowed to hold a slot indefinitely.
  • Use a restart policy (`restart: on-failure`) and a small queue/worker pool cap so the number of simultaneous Chrome processes never exceeds what the tier's RAM budget supports.
The combination of container memory limits and a hard concurrency cap in your queue is what actually prevents an out-of-memory kill from cascading into the OOM killer taking down unrelated processes on the same VPS. This pattern applies whether the rendering engine is Puppeteer or Playwright — both are Chromium-based Docker workloads and both benefit from the same containment approach.

Storage and datacenter location for rendering workloads

Rendering writes temporary Chromium profile data, screenshot output, and generated PDFs to disk continuously under load, and disk-backed temp storage (see the `--disable-dev-shm-usage` flag above) makes I/O speed matter more than it would for a typical web app. Every X-Zone Servers KVM VPS tier ships with SSD storage, which keeps that temp-write and read-back cycle fast enough that disk isn't the bottleneck once concurrency climbs into the dozens of instances shown in the table above. Bandwidth isn't a constraint either way — every tier ships with unmetered 1 Gbps, so a service producing large PDF batches or high-resolution screenshots isn't billed extra for output volume.
Latency between the VPS and the target URLs also adds up fast when a job renders hundreds of pages in sequence — a screenshot service in one region fetching pages hosted on another continent eats round-trip time on every asset a page loads before it can be captured. X-Zone Servers offers datacenters across Europe and the US, including Frankfurt, Amsterdam, Vienna, and Ashburn, so placing the VPS near the sites being rendered (or near the audience consuming the output) is usually just a matter of picking the closest location at deploy time.

Why self-hosting fits this workload specifically

A URL-to-screenshot or HTML-to-PDF service is close to the ideal self-hosting candidate: the workload is well-understood (Puppeteer or Playwright plus a queue), the resource ceiling is predictable once the RAM math above is run, and the traffic pattern is usually internal — invoices, previews, thumbnails — rather than public-facing and spiky. Full root access on a KVM VPS means installing exactly the Chromium build, fonts, and system libraries the rendering job needs, with no waiting on a SaaS vendor's font support or PDF options. Deploy time under 60 seconds on X-Zone Servers' KVM VPS lineup means testing a tier before committing to it is a non-issue, and hourly billing capped at the monthly rate means a short benchmarking run costs cents, not a full month.

Verdict

A self-hosted screenshot and PDF rendering API replaces per-request SaaS billing with a fixed monthly VPS cost, and the sizing math is straightforward once the per-instance RAM rule of thumb is applied against a tier's specs — Nano or Micro for light, personal-scale rendering, Basic through Business once concurrency climbs into the dozens of simultaneous Chrome instances. X-Zone Servers' KVM VPS lineup gives full root, Docker support, and SSD storage across every tier, so the resource limits that keep a runaway page from crashing the box are entirely under your control from day one. Start on the tier that matches your expected concurrency and resize later as volume grows.