High-RAM VPS for Playwright clusters: Contabo vs DigitalOcean vs Vultr on memory, CPU and I/O
Disclosure: some links in this guide are affiliate links. If you sign up through them, I may earn a commission at no extra cost to you. How this site makes money.
Contabo sells RAM for an eighth to a tenth of what DigitalOcean and Vultr charge: €25 a month (€20 for the first 24 months) buys 48 GB and 12 vCPUs, while 32 GB costs $160 to $168 a month on the other two. But Playwright rendering usually runs out of CPU before RAM, so extra memory only pays off when pages spend most of their time waiting, for example on per-domain rate limits or long sessions. Use Contabo for steady, waiting-bound workloads; Vultr Optimized Cloud or DigitalOcean CPU-Optimized for CPU-heavy rendering that needs dedicated vCPUs; and either one with hourly or per-second billing for jobs that run only a few hours a day. Measure memory per page and CPU steal on a one-month plan before you commit to a long term.
Headless Chromium is memory-hungry, so the obvious way to scale a Playwright cluster is to buy the most RAM per dollar. On that metric Contabo wins by a factor of eight to ten. But RAM is only one of three limits on a scraping box, and on most workloads it isn't the first one you hit. This guide compares Contabo, DigitalOcean and Vultr on all three, RAM, CPU and I/O, explains which workloads actually need high RAM, and gives you a script to measure a server before you sign a long contract.
Plan specs and prices come from Contabo's and DigitalOcean's pricing pages and Vultr's public plans API, checked in October 2026. We didn't benchmark the three providers. The probe script in section 4 was tested on a Windows workstation (8 threads) with Microsoft Edge against local synthetic product pages. Its output below shows how to read the results, not how any provider performs. Pages-per-server figures are our estimates, worded as such.
1. Three limits, and which one you hit first
- RAM sets how many pages can be open at once. Chromium's base process takes roughly 150–300 MB, and each open page in its own context adds roughly 100–300 MB on a typical product page with images blocked. Heavy single-page apps can take 400 MB or more.
- CPU sets how fast those pages finish. Parsing, layout and JavaScript are CPU work. A vCPU keeps roughly 2–4 rendering pages moving, by our estimate; past that, adding pages adds memory use and latency, not throughput.
- I/O and network rarely limit scraping. Contexts live in memory, and scraped pages are small once images are blocked. Disk speed matters when the same box runs your queue and database. Inbound traffic, which is what scraping mostly is, is free on all three providers.
The question that decides the plan is what your pages are doing most of the time:
A page waiting 3 seconds for a per-domain rate limit holds its memory but uses almost no CPU. That's the one case where 48 GB of RAM on 12 shared vCPUs is well balanced: you can keep 100+ pages open, most of them waiting. If your pages render back to back as fast as targets allow, CPU is the limit and most of that RAM sits idle.
2. The plans side by side
| Plan | vCPU | RAM | Disk | Traffic | Price/month | Price per GB RAM |
|---|---|---|---|---|---|---|
| 4 shared | 8 GB | 100 GB SSD | Unlimited*, 200 Mbit/s | €5.50 (€4.40 first 24 months) | €0.69 | |
| Contabo Cloud VPS 8 | 8 shared | 24 GB | 300 GB SSD or 150 GB NVMe | Unlimited*, 600 Mbit/s | €14.00 (€11.20) | €0.58 |
| Contabo Cloud VPS 12 | 12 shared | 48 GB | 400 GB SSD or 200 GB NVMe | Unlimited*, 800 Mbit/s | €25.00 (€20.00) | €0.52 |
| 8 shared | 16 GB | 320 GB SSD | 6,000 GiB | $96 | $6.00 | |
| DigitalOcean Memory-Optimized | 4 | 32 GB | 100 GB NVMe | 6,000 GiB | $168 | $5.25 |
| DigitalOcean CPU-Optimized | 8 dedicated | 16 GB | 100 GB | 6,000 GiB | $168 | $10.50 |
| 8 shared | 32 GB | 640 GB SSD | 6 TB | $160 | $5.00 | |
| Vultr Optimized Cloud, Memory | 4 dedicated | 32 GB | 200 GB NVMe | 8 TB | $160 | $5.00 |
| Vultr Optimized Cloud, CPU | 8 dedicated | 16 GB | 150 GB NVMe | 7 TB | $160 | $10.00 |
Prices from each provider in October 2026. Contabo prices are in euros: the first figure is the regular monthly price, the one in brackets the price Contabo shows for the first 24 months. Contabo's page can show prices with or without VAT depending on the country you select. *Contabo's traffic is unlimited under a fair use policy that allows throttling of exceptionally high or disruptive outgoing traffic. DigitalOcean's and Vultr's prices are in US dollars and exclude tax.
What fits on each, by our estimate
Assumptions: 1.5 GB reserved for the OS and the browser processes, 250 MB per open page, 2–3 rendering pages per shared vCPU and 3–4 per dedicated vCPU. These are starting points to verify with the probe in section 4, not measurements.
| Plan | Pages that fit in RAM | Pages the CPU keeps rendering | Limit when rendering |
|---|---|---|---|
| Contabo Cloud VPS 4 (8 GB, 4 shared) | about 26 | 8–12 | CPU |
| Contabo Cloud VPS 8 (24 GB, 8 shared) | about 92 | 16–24 | CPU |
| Contabo Cloud VPS 12 (48 GB, 12 shared) | about 190 | 24–36 | CPU |
| DigitalOcean CPU-Optimized (16 GB, 8 dedicated) | about 59 | 24–32 | CPU |
| Vultr Optimized Memory (32 GB, 4 dedicated) | about 124 | 12–16 | CPU |
| Vultr Regular (32 GB, 8 shared) | about 124 | 16–24 | CPU |
For rendering, every plan runs out of CPU first, so a memory-optimized plan is the wrong buy unless your pages mostly wait. The gap between the two columns is your room for waiting pages: on the Contabo VPS 12, roughly 150 pages can sit idle on rate limits while 24–36 render.
3. Each provider's trade-offs
Contabo: the most RAM per euro, with conditions
- Pro: RAM and disk at a fraction of the price elsewhere, an NVMe option on all but the smallest Cloud VPS, unlimited traffic, and an API and cloud-init for automated setup.
- Con: shared vCPUs with no stated ratio. Contabo describes its Core Cloud VPS line as optimized for cost efficiency on lighter workloads, and sells Performance VPS (AMD EPYC, NVMe) and Max Performance VPS (virtual dedicated) for sustained compute. Rendering is sustained compute. Expect CPU throughput to vary with neighbors on the host, and check CPU steal (section 4).
- Con: monthly terms, not hourly. The minimum term is one month, and follow-on terms must be at least as long as the first. You can't create 10 workers for a two-hour job and delete them. The lowest prices also depend on the 24-month rate.
- Our view: the right box for a steady, always-on queue with rate-limited targets, and for running Redis, Postgres and workers on one large machine. Start on a one-month term, run the probe, and commit to a longer term only if steal stays low over a few days.
DigitalOcean: predictable, per-second, with dedicated options
- Pro: per-second billing with a 60-second minimum from January 2026, a mature API and Terraform provider for creating workers on demand, and dedicated-vCPU CPU-Optimized and General Purpose plans when shared CPU isn't enough.
- Con: the most expensive RAM in this comparison. Basic Droplets are shared CPU, which DigitalOcean positions for bursty workloads that tolerate variable CPU.
- Our view: best for burst workers and for teams that already run their stack there. A CPU-Optimized 8 vCPU / 16 GB Droplet costs $168 a month, or $0.25 an hour (DigitalOcean's hourly rate is the monthly price over 672 hours). A two-hour daily job uses about 60 hours a month, about $15.
Vultr: the widest menu, hourly billing
- Pro: RAM at $5 per GB on both shared Regular and dedicated Optimized Cloud plans, separate CPU-, memory- and storage-optimized dedicated lines, AMD and Intel High Performance plans, and hourly billing.
- Con: many similar plans make it easy to pick the wrong one; Regular Cloud Compute uses standard SSD storage, while the Optimized Cloud plans use NVMe.
- Our view: the most flexible choice when you know your bottleneck. Optimized Cloud CPU (8 dedicated vCPUs, 16 GB, $160) for rendering-heavy work; Optimized Memory only if the probe shows you're waiting-bound and need dedicated CPU anyway.
A Contabo Cloud VPS 12 costs €25 a month whether it's busy or not. An 8 vCPU dedicated worker on DigitalOcean costs $0.25 an hour. If your jobs run fewer than roughly 110–120 hours a month, the hourly worker costs less and gives you dedicated CPU. Run them more than that, or keep a queue running all the time, and the flat box wins.
4. Measure before you commit: the probe script
Every number above is an estimate. This script measures the two that matter on the actual server, with your actual target pages: memory per open page, and the concurrency where throughput stops rising. On Linux it also reports CPU steal, the share of time the hypervisor gave your vCPUs to someone else, and I/O wait.
#!/usr/bin/env python3
"""Measure what a VPS really runs before you commit to it: memory per Playwright page,
pages per minute, CPU steal and I/O wait at rising concurrency.
pip install playwright psutil && playwright install chromium
python3 vps_probe.py urls.txt 2 4 8 16
Use 50+ URLs of the pages you will actually scrape, from sites you're allowed to load.
"""
import asyncio
import os
import re
import sys
import time
import psutil
from playwright.async_api import async_playwright
BLOCK = re.compile(r'\.(png|jpe?g|webp|gif|svg|woff2?|ttf|mp4|webm)(\?|$)')
PAGES_PER_SLOT = 3 # each concurrency level loads this many pages per slot
def tree_mb():
"""Resident memory of this script's child processes: the Playwright driver and every browser process."""
total = 0
for p in psutil.Process().children(recursive=True):
try:
total += p.memory_info().rss
except psutil.Error:
pass
return total / 2**20
async def run_level(browser, urls, slots):
queue = asyncio.Queue()
for i in range(slots * PAGES_PER_SLOT):
queue.put_nowait(urls[i % len(urls)])
stats = {'ok': 0, 'failed': 0, 'peak': 0.0}
async def worker():
while not queue.empty():
url = queue.get_nowait()
ctx = await browser.new_context() # one context per page, as a scraper would isolate jobs
page = await ctx.new_page()
await page.route(BLOCK, lambda r: r.abort())
try:
await page.goto(url, wait_until='load', timeout=60000)
await page.wait_for_timeout(1000) # let late scripts run, as extraction would
stats['ok'] += 1
except Exception:
stats['failed'] += 1
finally:
await ctx.close()
async def sampler():
while True:
stats['peak'] = max(stats['peak'], tree_mb())
await asyncio.sleep(0.25)
cpu0, t0 = psutil.cpu_times(), time.time()
sampling = asyncio.create_task(sampler())
await asyncio.gather(*(worker() for _ in range(slots)))
sampling.cancel()
cpu1, elapsed = psutil.cpu_times(), time.time() - t0
delta = {f: getattr(cpu1, f) - getattr(cpu0, f) for f in cpu1._fields}
total = sum(delta.values()) or 1
stats.update(elapsed=elapsed,
busy=100 * (total - delta['idle'] - delta.get('iowait', 0)) / total,
steal=100 * delta['steal'] / total if 'steal' in delta else None, # Linux only
iowait=100 * delta['iowait'] / total if 'iowait' in delta else None)
return stats
async def main():
urls = [u.strip() for u in open(sys.argv[1]) if u.strip()]
levels = [int(x) for x in sys.argv[2:]] or [2, 4, 8]
avail = psutil.virtual_memory().available / 2**20
print('%d vCPU, %.0f MB RAM available' % (psutil.cpu_count(), avail))
print('%5s %7s %8s %9s %8s %6s %6s %7s' % (
'pages', 'ok/fail', 'pages/m', 'peak MB', 'MB/page', 'cpu%', 'steal', 'iowait'))
fmt = lambda v: '%5.1f%%' % v if v is not None else ' n/a'
rows = []
async with async_playwright() as p:
browser = await p.chromium.launch(channel=os.environ.get('PW_CHANNEL'),
args=['--disable-dev-shm-usage'])
base = tree_mb() # driver + idle browser
for slots in levels:
s = await run_level(browser, urls, slots)
per_page = (s['peak'] - base) / slots
ppm = s['ok'] * 60 / s['elapsed']
rows.append((slots, ppm, per_page, s['busy']))
print('%5d %3d/%-3d %8.1f %9.0f %8.0f %6.0f %s %s' % (
slots, s['ok'], s['failed'], ppm, s['peak'], per_page, s['busy'],
fmt(s['steal']), fmt(s['iowait'])))
await browser.close()
per_page = max(r[2] for r in rows) # plan for the worst level measured
fit = int((avail * 0.8 - base) / per_page) if per_page > 0 else 0
best = max(rows, key=lambda r: r[1])
print('\nmemory: about %d concurrent pages fit in 80%% of available RAM at %.0f MB per page'
% (fit, per_page))
print('cpu: throughput peaked at %d pages (%.1f pages/min, %.0f%% CPU busy)%s' % (
best[0], best[1], best[3], ', the highest level tested: try more' if best[0] == levels[-1] else ''))
print('start at min(memory, cpu): %d concurrent pages' % min(fit, best[0]))
if __name__ == '__main__':
asyncio.run(main())
Output from our test machine, with synthetic product pages that build 4,000 DOM rows and run 150 ms of JavaScript each:
8 vCPU, 2215 MB RAM available
pages ok/fail pages/m peak MB MB/page cpu% steal iowait
4 12/0 128.8 1049 192 46 n/a n/a
8 24/0 194.1 1530 156 52 n/a n/a
16 48/0 231.7 2485 138 60 n/a n/a
24 72/0 228.0 4021 156 59 n/a n/a
memory: about 7 concurrent pages fit in 80% of available RAM at 192 MB per page
cpu: throughput peaked at 16 pages (231.7 pages/min, 60% CPU busy)
start at min(memory, cpu): 7 concurrent pages
How to read it:
- The knee. Going from 16 to 24 pages added no throughput but 1.5 GB of memory. That's the CPU ceiling, even though the CPU column shows only 60%: browsers also wait on their own main thread and on the network.
- Memory per page is the worst level measured, which is the one to plan with. It includes shared browser memory, so it overstates slightly: a safe error.
- Steal is n/a on Windows. On a Linux VPS, steal that stays above a few percent under load means neighbors are using your cores. If it's high at the knee, a dedicated-vCPU plan will often outperform a shared plan with more cores.
- Run it at different times of day on shared plans. A quiet Sunday night on a shared host can look very different from Monday afternoon.
5. Getting more pages out of the same RAM
- One browser, many contexts. Each extra browser process costs its base memory again. Use one Chromium per worker process and open a fresh context per job, as the probe does.
- Recycle the browser every few hundred pages. Chromium's memory creeps up over long runs. Restarting the browser after, say, 500 pages costs a second and resets it.
- Block images, fonts and media with
page.route. It cuts memory, CPU and proxy bandwidth at once, and product data rarely needs them. - Give Chromium shared memory. Docker's default
/dev/shmis 64 MB, and Chromium crashes tabs when it runs out. Setshm_size: 1gbin Compose, or launch with--disable-dev-shm-usage, which uses/tmpinstead. - Use swap as a crash buffer, not capacity. A 2–4 GB swap file stops the kernel's OOM killer from taking down a worker on a spike. If the server swaps steadily, lower concurrency; swapped Chromium is slower than fewer pages.
- Wait for the data, not the network.
wait_until='domcontentloaded'plus waiting for the price selector frees a page sooner thannetworkidle, which waits for trackers and chat widgets. - Cap each worker's memory with container limits so one runaway page kills one worker, not the server. Our self-hosted Playwright cluster guide has the Docker Compose setup and a memory budget for a 4 GB server.
- Datacenter IPs are the same problem everywhere. Bot-protection services treat hosting-provider IP ranges as higher risk, whichever provider you choose. For protected targets, plan on proxies or a scraping API; see ZenRows vs self-hosted Playwright. To avoid running browsers at all, compare Browserless vs a VPS cluster.
6. Tools
Top picks
| Tool | Best for | Why it fits |
|---|---|---|
| Always-on, waiting-bound queues; one big box for workers, Redis and database | 48 GB RAM and 12 vCPUs for €25 a month, an eighth to a tenth of the price per GB elsewhere, with NVMe and unlimited traffic. Shared vCPUs and monthly terms, so measure steal before a long commitment. | |
| Burst workers, dedicated CPU, teams already on DigitalOcean | Per-second billing, a mature API for creating and destroying workers, and dedicated CPU-Optimized plans for rendering-heavy jobs. | |
| Picking the exact CPU-to-RAM ratio; dedicated CPU at the lowest list price | $5 per GB of RAM on shared and dedicated plans, separate CPU-, memory- and storage-optimized lines, and hourly billing. |
Alternatives
We don't earn anything from these links.
- Hetzner Cloud: another low-cost European provider with hourly billing and shared and dedicated vCPU plans, worth comparing with Contabo on price per GB.
- Playwright for Python and psutil: the two libraries the probe script uses.
7. Pitfalls
- Buying RAM for a CPU-bound job. If the probe's knee comes long before memory runs out, more RAM won't speed you up. Buy vCPUs, preferably dedicated.
- Locking in a 24-month price before testing. The longest terms carry the lowest price but also the most risk. Test for a month first.
- Running the probe against pages unlike your targets. A lightweight test page makes any server look good. Use 50 or more real URLs from sites you're allowed to scrape.
- Ignoring CPU steal on shared plans. Two servers with the same vCPU count can render at very different speeds. Steal is the number that explains it.
- Keeping the cluster's monitoring on the cluster. If the big box dies, so do its alerts. See our e-commerce and scraper monitoring guide for heartbeat checks that live elsewhere.
Contabo sells RAM for an eighth to a tenth of what DigitalOcean and Vultr charge, and for always-on queues where pages mostly wait, that's the best value. For rendering-heavy work, CPU runs out before RAM on every plan we compared, so dedicated vCPUs on Vultr Optimized Cloud or DigitalOcean CPU-Optimized are the better buy. For jobs that run under about 110 hours a month, hourly workers beat any flat plan. Whichever you pick, run the probe on a one-month term before you commit.