HomeStart / GuidesAnleitungen / Web scraping from a VPS/ Web-Scraping von einem VPS aus

Web scraping from a VPS: Python, Playwright, and where the memory actually goesWeb-Scraping von einem VPS aus: Python, Playwright und wohin der Speicher tatsächlich geht

A scraper that works on your laptop and dies on a server almost always dies the same way: it opens too many tabs, none of them ever close, and the kernel kills the process at 2 a.m. with no explanation in your logs. None of that is a Playwright problem. It is a "nobody told me where the memory goes" problem.Ein Scraper, der auf deinem Laptop funktioniert und auf einem Server stirbt, stirbt fast immer auf dieselbe Weise: Er öffnet zu viele Tabs, keiner von ihnen schließt jemals, und der Kernel killt den Prozess um zwei Uhr nachts, ohne eine Erklärung in deinen Logs zu hinterlassen. Nichts davon ist ein Playwright-Problem. Es ist ein „Mir hat niemand gesagt, wohin der Speicher geht“-Problem.

What you are actually runningWas du hier eigentlich betreibst

A scraping job on a VPS is three separate concerns wearing one hoodie: Python to drive the logic, Playwright to control a real browser, and headless Chromium to render pages that would not give up their data to a plain HTTP client. You only need all three when the site renders content with JavaScript. If the data you want is sitting in a JSON response or plain HTML, skip the browser entirely and use requests or httpx — it is ten times lighter and there is no memory story to manage at all.Ein Scraping-Job auf einem VPS ist eigentlich drei getrennte Zuständigkeiten in einem Hoodie: Python treibt die Logik an, Playwright steuert einen echten Browser, und headless Chromium rendert Seiten, die ihre Daten einem einfachen HTTP-Client nicht preisgeben würden. Alle drei brauchst du nur, wenn die Seite Inhalte per JavaScript rendert. Liegen die Daten, die du willst, in einer JSON-Antwort oder in reinem HTML, verzichte ganz auf den Browser und nutze requests oder httpx — das ist zehnmal leichter, und es gibt überhaupt keine Speicherfrage zu verwalten.

Playwright is the part worth pinning by version, because its Python package and the browser build it downloads are matched to each other:Playwright ist der Teil, den es sich lohnt, per Version zu pinnen, weil sein Python-Paket und der Browser-Build, den es herunterlädt, aufeinander abgestimmt sind:

pip install playwright==1.62.0
playwright install --with-deps chromium

install --with-deps pulls the system libraries Chromium needs (font rendering, audio stubs, GTK bits) alongside the browser binary itself, which is the difference between "it launches" and a wall of missing-.so errors on a fresh machine.install --with-deps zieht neben dem Browser-Binary selbst auch die System-Bibliotheken, die Chromium braucht (Font-Rendering, Audio-Stubs, GTK-Bausteine) — das ist der Unterschied zwischen „es startet“ und einer Wand von fehlenden .so-Fehlern auf einer frischen Maschine.

Read this before you buy: outbound only, so NAT does not matterLies das, bevor du kaufst: nur ausgehend, NAT spielt also keine Rolle

Some guides on this site spend a whole section on NAT IPv4 versus a dedicated IPv4, because the workload needs the outside world to reach in — a webhook, a game server, a Discord bot's voice gateway. A scraper is the opposite shape. It opens connections out to the sites you are reading and never listens for anything coming back on a public port. NAT IPv4 is therefore a non-issue here: whatever address scheme the VPS gives you, outbound HTTPS to the sites you are scraping works the same way. Do not spend time on it.Manche Anleitungen auf dieser Seite widmen NAT-IPv4 gegenüber einer dedizierten IPv4 einen ganzen Abschnitt, weil die Last darauf angewiesen ist, dass die Außenwelt hineinkommt — ein Webhook, ein Gameserver, das Voice-Gateway eines Discord-Bots. Ein Scraper hat die entgegengesetzte Form. Er baut Verbindungen nach außen zu den Seiten auf, die du liest, und lauscht niemals auf irgendetwas, das über einen öffentlichen Port zurückkommt. NAT-IPv4 ist hier deshalb kein Thema: Welches Adressschema dein VPS dir auch gibt, ausgehendes HTTPS zu den Seiten, die du scrapst, funktioniert genauso. Verschwende keine Zeit damit.

The one thing that does matter here is ordinary outbound connectivity, which any Ubuntu LTS image has working out of the box.Das einzige, was hier wirklich zählt, ist gewöhnliche ausgehende Konnektivität, die bei jedem Ubuntu-LTS-Image direkt ab Werk funktioniert.

Step 1: the machineSchritt 1: die Maschine

Deploy an Ubuntu LTS image, then do the ordinary hardening before you install anything that talks to the internet on your behalf: a non-root user, an SSH key, password login switched off, and a firewall that defaults to deny. connect to your VPS over SSH and secure your VPS cover both.Setze ein Ubuntu-LTS-Image auf, und erledige dann die übliche Härtung, bevor du irgendetwas installierst, das in deinem Namen mit dem Internet spricht: ein Non-Root-User, ein SSH-Key, Passwort-Login deaktiviert, und eine Firewall, die standardmäßig blockiert. über SSH mit deinem VPS verbinden und deinen VPS absichern decken beides ab.

sudo apt update
sudo apt install -y python3-venv python3-pip git

Chromium also needs a handful of OS-level libraries (font rendering, audio stubs, GTK bits) that only root can install. Do that now, while you are still the admin user, with a throwaway venv that exists only to give you the playwright CLI:Chromium braucht außerdem eine Handvoll Bibliotheken auf Betriebssystem-Ebene (Font-Rendering, Audio-Stubs, GTK-Bausteine), die nur root installieren kann. Erledige das jetzt, solange du noch der Admin-User bist, mit einem Wegwerf-venv, das nur dazu existiert, dir die playwright-CLI zu geben:

python3 -m venv /tmp/playwright-setup
/tmp/playwright-setup/bin/pip install playwright==1.62.0
sudo /tmp/playwright-setup/bin/playwright install-deps chromium
rm -rf /tmp/playwright-setup

Create the scraper's own user rather than running it as yourself or as root — a compromised or buggy scraping script should not have write access to anything else on the box.Leg für den Scraper einen eigenen User an, statt ihn als dich selbst oder als root laufen zu lassen — ein kompromittiertes oder fehlerhaftes Scraping-Skript sollte keinen Schreibzugriff auf sonst irgendetwas auf der Box haben.

sudo useradd -m -s /bin/bash scraper
sudo -u scraper -i

Step 2: Python and Playwright in a virtual environmentSchritt 2: Python und Playwright in einer virtuellen Umgebung

Ubuntu 24.04 marks the system Python as externally managed, so a bare pip install outside a virtual environment refuses to run. A venv sidesteps that and keeps the scraper's dependencies away from anything else on the machine:Ubuntu 24.04 markiert das System-Python als externally managed, sodass ein nacktes pip install außerhalb einer virtuellen Umgebung die Ausführung verweigert. Ein venv umgeht das und hält die Abhängigkeiten des Scrapers von allem anderen auf der Maschine fern:

python3 -m venv ~/scraper-env
source ~/scraper-env/bin/activate
pip install --upgrade pip
pip install playwright==1.62.0
playwright install chromium

That is playwright install chromium, not --with-deps — the OS libraries --with-deps would otherwise fetch with sudo apt-get are already on the box from Step 1, so this only downloads the browser binary into ~/.cache/ms-playwright. No root needed, which is the point: the scraper user has none, on purpose, and never should.Das ist playwright install chromium, nicht --with-deps — die Betriebssystem-Bibliotheken, die --with-deps sonst per sudo apt-get holen würde, sind aus Schritt 1 bereits auf der Box, also lädt das hier nur das Browser-Binary nach ~/.cache/ms-playwright herunter. Kein root nötig, und das ist der Punkt: Der scraper-User hat absichtlich keins, und das soll auch so bleiben.

A minimal fetch, sequential and polite, looks like this:Ein minimaler Fetch, sequenziell und höflich, sieht so aus:

import asyncio
from playwright.async_api import async_playwright

async def fetch(url: str) -> str:
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page(
            user_agent="MyScraper/1.0 (+https://example.com/bot; [email protected])"
        )
        await page.goto(url, timeout=30_000, wait_until="domcontentloaded")
        html = await page.content()
        await browser.close()
        return html

asyncio.run(fetch("https://example.com"))

async with async_playwright() matters more than it looks: it guarantees the browser process is torn down in a finally block even if page.goto throws, which is exactly the case that otherwise leaves an orphaned Chromium process sitting on your VPS.async with async_playwright() ist wichtiger, als es aussieht: Es garantiert, dass der Browser-Prozess in einem finally-Block abgebaut wird, selbst wenn page.goto eine Exception wirft — genau der Fall, der sonst einen verwaisten Chromium-Prozess auf deinem VPS zurücklässt.

Step 3: being the kind of scraper sites tolerateSchritt 3: der Scraper sein, den Websites tolerieren

Four habits separate a scraper that gets IP-banned in a day from one that runs for years unnoticed.Vier Gewohnheiten trennen einen Scraper, der binnen eines Tages per IP gebannt wird, von einem, der jahrelang unbemerkt läuft.

Read robots.txt before you fetch anything. Python's standard library already has a parser, so there is no reason to skip this:Lies robots.txt, bevor du irgendetwas fetchst. Pythons Standardbibliothek hat bereits einen Parser dafür, es gibt also keinen Grund, das zu überspringen:

from urllib.robotparser import RobotFileParser

rp = RobotFileParser()
rp.set_url("https://example.com/robots.txt")
rp.read()
allowed = rp.can_fetch("MyScraper/1.0", "https://example.com/page")

If robots.txt sets a Crawl-delay, honour it; rp.crawl_delay("MyScraper/1.0") reads it back.Setzt robots.txt einen Crawl-delay, halte dich daran; rp.crawl_delay("MyScraper/1.0") liest ihn dir aus.

Rate-limit yourself deliberately, rather than firing requests as fast as the event loop allows. An asyncio.Semaphore bounding concurrent pages, plus a fixed asyncio.sleep between requests to the same host, is enough for the overwhelming majority of scraping jobs and costs you almost nothing in throughput.Bremse dich bewusst selbst, statt Requests so schnell abzufeuern, wie die Event-Loop es zulässt. Ein asyncio.Semaphore, das gleichzeitige Seiten begrenzt, plus ein fester asyncio.sleep zwischen Requests an denselben Host reicht für die überwältigende Mehrheit der Scraping-Jobs und kostet dich beim Durchsatz fast nichts.

Identify yourself in the User-Agent. MyScraper/1.0 (+https://example.com/bot; [email protected]) costs one line and means a site owner who notices your traffic has somewhere to write instead of just blocking your address range. A generic browser User-Agent pretending to be Chrome fools nobody who is actually looking, and it removes the one signal that lets a cooperative site owner reach you before reaching for a block rule.Gib dich im User-Agent zu erkennen. MyScraper/1.0 (+https://example.com/bot; [email protected]) kostet eine Zeile und gibt einem Seitenbetreiber, dem dein Traffic auffällt, eine Adresse zum Schreiben, statt nur deinen Adressbereich zu blockieren. Ein generischer Browser-User-Agent, der Chrome vorspielt, täuscht niemanden, der wirklich hinschaut, und nimmt das einzige Signal weg, mit dem ein kooperativer Seitenbetreiber dich erreichen kann, bevor er zur Blockregel greift.

Cache what you already fetched. Store the response body keyed by URL, alongside the ETag or Last-Modified header if the site sends one, and send If-None-Match or If-Modified-Since on the next pass. A 304 Not Modified response costs the target site almost nothing and saves you a full page render. A local SQLite file with one table — url, etag, fetched_at, body_path — does the whole job without adding a dependency.Cache, was du schon abgerufen hast. Speichere den Response-Body mit der URL als Key, zusammen mit dem ETag- oder Last-Modified-Header, falls die Seite einen sendet, und schicke beim nächsten Durchlauf If-None-Match oder If-Modified-Since mit. Eine 304 Not Modified-Antwort kostet die Zielseite fast nichts und erspart dir ein komplettes Seiten-Rendering. Eine lokale SQLite-Datei mit einer Tabelle — url, etag, fetched_at, body_path — erledigt den ganzen Job, ohne eine weitere Abhängigkeit hinzuzufügen.

Step 4: retries and checkpointsSchritt 4: Retries und Checkpoints

Networks fail, sites time out, and a page occasionally hangs waiting for a request that will never resolve. Wrap each fetch with a bounded retry and an explicit backoff, rather than letting one flaky page crash the whole run:Netzwerke fallen aus, Seiten laufen in Timeouts, und gelegentlich hängt eine Seite fest und wartet auf einen Request, der nie eine Antwort liefert. Statte jeden Fetch mit einem begrenzten Retry und einem expliziten Backoff aus, statt eine einzige wacklige Seite den ganzen Lauf abstürzen zu lassen:

import asyncio

async def fetch_with_retry(url: str, attempts: int = 3) -> str | None:
    for attempt in range(1, attempts + 1):
        try:
            return await fetch(url)
        except Exception as exc:
            if attempt == attempts:
                return None
            await asyncio.sleep(2 ** attempt)

Retries handle a single bad request. They do not help when the process itself dies partway through a list of ten thousand URLs — which is exactly what happens the first time the OOM killer or a systemctl restart catches it mid-run. The fix is a checkpoint: record which URLs are done as you go, and let the script skip them on the next start instead of beginning over.Retries fangen einen einzelnen schlechten Request ab. Sie helfen nicht, wenn der Prozess selbst mittendrin in einer Liste von zehntausend URLs stirbt — genau das passiert, sobald der OOM-Killer oder ein systemctl restart ihn mitten im Lauf erwischt. Die Lösung ist ein Checkpoint: Halte fest, welche URLs schon erledigt sind, während du vorankommst, und lass das Skript sie beim nächsten Start überspringen, statt von vorn zu beginnen.

DONE_FILE = "done_urls.txt"

def load_done() -> set[str]:
    try:
        with open(DONE_FILE) as f:
            return {line.strip() for line in f}
    except FileNotFoundError:
        return set()

def mark_done(url: str) -> None:
    with open(DONE_FILE, "a") as f:
        f.write(url + "\n")
        f.flush()

Append-only, flushed after every line, so a kill signal mid-run loses at most the one URL in flight rather than the whole job's progress.Nur anhängend, nach jeder Zeile geflusht, sodass ein Kill-Signal mitten im Lauf höchstens die eine gerade laufende URL verliert, statt den Fortschritt des ganzen Jobs.

Step 5: memory, and where it actually goesSchritt 5: Speicher, und wohin er tatsächlich geht

This is the part that decides which tier you need. Headless Chromium is a multi-process browser even with one tab open: a main browser process plus a separate renderer process per page. Idle, a single headless instance with one page open sits around 150–250 MB. A heavy modern page — ad scripts, a JavaScript framework, web fonts, tracking pixels — can push that single renderer to 300–500 MB while it is loading. That is the number that matters for sizing, and it is per concurrent page, not per script.Das ist der Teil, der entscheidet, welchen Tarif du brauchst. Headless Chromium ist ein Multi-Prozess-Browser, selbst mit nur einem offenen Tab: ein Hauptbrowser-Prozess plus ein eigener Renderer-Prozess pro Seite. Im Leerlauf liegt eine einzelne headless Instanz mit einer offenen Seite bei rund 150–250 MB. Eine schwere, moderne Seite — Werbeskripte, ein JavaScript-Framework, Web-Fonts, Tracking-Pixel — kann diesen einen Renderer während des Ladens auf 300–500 MB treiben. Das ist die Zahl, die für die Dimensionierung zählt, und sie gilt pro gleichzeitig offener Seite, nicht pro Skript.

On a 1 GiB Starter, that means: one page at a time, not a pool of ten tabs racing each other. Sequential fetching with the checkpoint pattern above is not a compromise here, it is the correct design for this amount of RAM. If you genuinely need several pages rendering concurrently, that is the point where you size up a tier rather than fighting the ceiling with launch flags.Auf einem 1-GiB-Starter heißt das: eine Seite nach der anderen, kein Pool aus zehn Tabs, die gegeneinander antreten. Sequenzielles Fetching mit dem Checkpoint-Muster von oben ist hier kein Kompromiss, sondern das richtige Design für diese Menge RAM. Brauchst du wirklich mehrere Seiten gleichzeitig gerendert, ist das der Punkt, an dem du einen Tarif höher gehst, statt mit Launch-Flags gegen die Decke zu kämpfen.

A few flags earn their keep regardless of tier:Ein paar Flags lohnen sich unabhängig vom Tarif:

browser = await p.chromium.launch(
    headless=True,
    args=["--disable-dev-shm-usage", "--disable-gpu"],
)

--disable-dev-shm-usage makes Chromium use disk-backed temp files instead of /dev/shm for its shared memory, which avoids crashes on machines where /dev/shm is sized small. --disable-gpu skips GPU compositing you will never use on a headless server anyway.--disable-dev-shm-usage lässt Chromium für seinen Shared Memory plattenbasierte Temp-Dateien statt /dev/shm nutzen, was Abstürze auf Maschinen vermeidet, bei denen /dev/shm klein dimensioniert ist. --disable-gpu überspringt GPU-Compositing, das du auf einem headless Server ohnehin nie nutzen wirst.

One flag you will see copied everywhere and should leave alone: --no-sandbox. It exists for containers that run as root without the kernel permissions Chromium's sandbox needs. A KVM VPS gives you your own kernel and normal user permissions, so the sandbox works as intended — dropping it removes a real security boundary for no benefit here.Ein Flag, das du überall kopiert siehst und das du in Ruhe lassen solltest: --no-sandbox. Es existiert für Container, die als root laufen, ohne die Kernel-Rechte, die Chromiums Sandbox braucht. Ein KVM-VPS gibt dir einen eigenen Kernel und normale User-Rechte, sodass die Sandbox wie vorgesehen funktioniert — sie wegzulassen entfernt eine echte Sicherheitsgrenze, ohne dass es hier irgendeinen Vorteil bringt.

Watch actual usage rather than guessing: free -h for the headline number, and ps aux --sort=-%mem | grep chrome when you want to see the renderer processes individually.Beobachte die tatsächliche Nutzung, statt zu raten: free -h für die Kopfzahl, und ps aux --sort=-%mem | grep chrome, wenn du die Renderer-Prozesse einzeln sehen willst.

Disk is the quieter constraint. The Chromium build Playwright downloads is around 300–400 MB on disk once its dependencies are counted, and it lives under ~/.cache/ms-playwright. Add whatever you are saving per page — rendered HTML, screenshots, the cache database from Step 3 — and a long-running crawl can fill a Starter's disk well before it runs short on RAM. Check usage with du -sh ~/.cache/ms-playwright ~/scrape-cache.db occasionally, and see add extra storage to your VPS if a crawl genuinely needs more room than the Starter ships with.Speicherplatz ist die leisere Grenze. Der Chromium-Build, den Playwright herunterlädt, liegt inklusive seiner Abhängigkeiten bei rund 300–400 MB auf der Platte und lebt unter ~/.cache/ms-playwright. Rechne dazu, was du pro Seite sonst noch speicherst — gerendertes HTML, Screenshots, die Cache-Datenbank aus Schritt 3 —, und ein lang laufender Crawl kann die Platte eines Starters füllen, lange bevor ihm der RAM ausgeht. Prüfe die Nutzung gelegentlich mit du -sh ~/.cache/ms-playwright ~/scrape-cache.db, und sieh dir zusätzlichen Speicher zu deinem VPS hinzufügen an, falls ein Crawl wirklich mehr Platz braucht, als der Starter mitbringt.

Step 6: running it on a schedule with systemd timersSchritt 6: nach Zeitplan laufen lassen mit systemd-Timern

Cron works, but a systemd timer gives you structured logs in journalctl, environment handling that does not depend on cron's minimal shell, and a Persistent=true option that catches a run the VPS missed because it happened to be rebooting.Cron funktioniert, aber ein systemd-Timer gibt dir strukturierte Logs in journalctl, eine Umgebungsverwaltung, die nicht von Crons minimaler Shell abhängt, und eine Persistent=true-Option, die einen Lauf nachholt, den der VPS verpasst hat, weil er gerade neu gestartet ist.

# /etc/systemd/system/scraper.service
[Unit]
Description=Run the scraper once

[Service]
Type=oneshot
User=scraper
WorkingDirectory=/home/scraper
ExecStart=/home/scraper/scraper-env/bin/python /home/scraper/scrape.py
MemoryMax=800M
OOMPolicy=kill
# /etc/systemd/system/scraper.timer
[Unit]
Description=Run the scraper every 6 hours

[Timer]
OnCalendar=*-*-* 00/6:00:00
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now scraper.timer
journalctl -u scraper.service -f

MemoryMax=800M on a 1 GiB Starter is deliberate headroom under the ceiling, not an arbitrary number: it leaves room for SSH, systemd itself, and anything else running on the box before a runaway scraper starts pressuring the kernel's own OOM killer. OOMPolicy=kill tells systemd to end the whole cgroup cleanly when that limit is hit, so the timer's next scheduled run starts from a known state instead of inheriting a half-alive process tree.MemoryMax=800M auf einem 1-GiB-Starter ist bewusster Spielraum unter der Decke, keine willkürliche Zahl: Es lässt Platz für SSH, systemd selbst und alles andere, was auf der Box läuft, bevor ein außer Kontrolle geratener Scraper anfängt, den Kernel-eigenen OOM-Killer unter Druck zu setzen. OOMPolicy=kill sagt systemd, die ganze Cgroup sauber zu beenden, wenn dieses Limit erreicht ist, sodass der nächste geplante Lauf des Timers von einem bekannten Zustand startet, statt einen halb lebendigen Prozessbaum zu erben.

The thing that bites laterDas Ding, das später zuschlägt

The failure mode that actually costs people a weekend is not a selector breaking. It is Chromium processes that never got told to stop. If the script is killed by something other than its own code — an OOM kill, a systemctl stop, an SSH session dropping mid-run without a service wrapper — the async with async_playwright() cleanup never runs, and the renderer processes it spawned can be left as orphans, still holding memory, invisible unless you go looking with ps. Do that a few times over a few weeks of a scraper that occasionally crashes, and the box is slowly filling with dead Chromium processes until a run that would otherwise fit inside 1 GiB does not.Das Fehlerbild, das Leute tatsächlich ein Wochenende kostet, ist kein kaputter Selektor. Es sind Chromium-Prozesse, denen nie gesagt wurde, dass sie aufhören sollen. Wird das Skript durch etwas anderes als seinen eigenen Code beendet — ein OOM-Kill, ein systemctl stop, eine SSH-Session, die mitten im Lauf ohne Service-Wrapper abbricht —, läuft das async with async_playwright()-Cleanup nie, und die davon gestarteten Renderer-Prozesse können als Waisen zurückbleiben, halten weiter Speicher und sind unsichtbar, sofern du nicht mit ps danach suchst. Passiert das ein paar Mal über einige Wochen mit einem Scraper, der gelegentlich abstürzt, füllt sich die Box langsam mit toten Chromium-Prozessen, bis ein Lauf, der sonst locker in 1 GiB passen würde, es nicht mehr tut.

The MemoryMax and OOMPolicy=kill pair above is exactly the fix: it makes systemd own the whole process group the browser spawned, so a kill actually kills everything underneath it instead of leaving stragglers. If you are debugging a VPS that has quietly gotten slower over weeks with no code change, ps aux | grep -c chrome before you look anywhere else is worth the ten seconds.Das Paar aus MemoryMax und OOMPolicy=kill von oben ist genau die Lösung dafür: Es macht systemd zum Besitzer der ganzen Prozessgruppe, die der Browser gestartet hat, sodass ein Kill wirklich alles darunter killt, statt Nachzügler zurückzulassen. Debuggst du einen VPS, der über Wochen still langsamer geworden ist, ohne dass sich am Code etwas geändert hätte, lohnen sich die zehn Sekunden für ps aux | grep -c chrome, bevor du woanders suchst.

On overnight.hostBei overnight.host

Full disclosure: this is what we sell. A 1 GiB Starter runs one headless Chromium session comfortably; move up a tier once you need several browser contexts running at the same time rather than one after another.Zur vollen Transparenz: Das ist, was wir verkaufen. Ein 1-GiB-Starter fährt eine headless-Chromium-Session bequem; steige einen Tarif höher, sobald du mehrere Browser-Kontexte gleichzeitig statt nacheinander brauchst.

Linux KVM VPS — EUR 4.99 to EUR 59.99 a month, on our own single-tenant bare metal in Dallas, TX and Charlotte, NC. Full hardware virtualisation (KVM), your own kernel, full root. Six tiers, vps-starter to vps-ultra. Starter is 1 vCPU, 1 GiB RAM, 25 GB disk.Linux-KVM-VPS — 4,99 bis 59,99 EUR im Monat, auf unserer eigenen Single-Tenant-Bare-Metal-Hardware in Dallas, TX und Charlotte, NC. Vollständige Hardware-Virtualisierung (KVM), eigener Kernel, volles Root. Sechs Tarife, vps-starter bis vps-ultra. Starter hat 1 vCPU, 1 GiB RAM, 25 GB Speicherplatz.

You order in the shop, pay by card (Stripe) or SEPA bank transfer, and your login details are e-mailed to you once the service is set up. Support is e-mail, run by one person, with no guaranteed response time. All prices are final totals under the German small-business rule (§19 UStG); no VAT is added or shown.Du bestellst im Shop, zahlst per Karte (Stripe) oder SEPA-Überweisung, und deine Zugangsdaten werden dir per E-Mail zugeschickt, sobald der Dienst eingerichtet ist. Support läuft per E-Mail, von einer einzelnen Person betrieben, ohne garantierte Reaktionszeit. Alle Preise sind Endpreise. Gemäß § 19 UStG wird keine Umsatzsteuer ausgewiesen.

Order vps-starter → · Linux KVM VPS overviewvps-starter bestellen → · Übersicht Linux-KVM-VPS

Written by the person who runs overnight.host: a small, honest hosting company on dedicated bare metal — Linux VPS, game servers, web hosting. Live status at up.overnight.host.Geschrieben von der Person, die overnight.host betreibt: ein kleines, ehrliches Hosting-Unternehmen auf dedizierter Bare-Metal-Hardware — Linux-VPS, Gameserver, Webhosting. Live-Status unter up.overnight.host.

Technical guidance is informational. Plans, specifications and final prices are listed in the shop and can be ordered directly; VPS, game server, web hosting, one-click app and automation plans are provisioned automatically after payment. Custom configurations are still arranged by e-mail.Technische Hinweise dienen der Information. Pläne, Spezifikationen und Endpreise stehen im Shop und können direkt bestellt werden; VPS-, Gameserver-, Webhosting-, One-Click-App- und Automatisierungs-Pläne werden nach der Zahlung automatisch bereitgestellt. Sonderkonfigurationen werden weiterhin per E-Mail vereinbart.

FAQFAQ

Do I need a dedicated IPv4 to scrape from a VPS?Brauche ich eine dedizierte IPv4, um von einem VPS aus zu scrapen?

No. Scraping only opens outbound connections to the sites you read, and NAT IPv4 does not affect outbound traffic at all. A dedicated IPv4 only matters for services that need to accept inbound connections on a public port, which a scraper never does.Nein. Scraping baut nur ausgehende Verbindungen zu den Seiten auf, die du liest, und NAT-IPv4 beeinträchtigt ausgehenden Traffic überhaupt nicht. Eine dedizierte IPv4 spielt nur für Dienste eine Rolle, die eingehende Verbindungen auf einem öffentlichen Port annehmen müssen, was ein Scraper nie tut.

How much RAM does headless Chromium actually need?Wie viel RAM braucht headless Chromium tatsächlich?

Roughly 150–250 MB idle for a single page, and up to 300–500 MB while rendering a heavy modern site, per concurrently open page. A 1 GiB Starter comfortably runs one page at a time; add RAM when you need several pages rendering in parallel rather than one after another.Etwa 150–250 MB im Leerlauf für eine einzelne Seite, und bis zu 300–500 MB während des Renderns einer schweren modernen Seite, pro gleichzeitig offener Seite. Ein 1-GiB-Starter fährt bequem eine Seite nach der anderen; leg RAM drauf, sobald du mehrere Seiten parallel statt nacheinander rendern musst.

Should I use Playwright, or is a plain HTTP client enough?Sollte ich Playwright nutzen, oder reicht ein einfacher HTTP-Client?

If the data you want is in the raw HTML or an API response the page calls, skip the browser and use requests or httpx — it is far lighter and there is nothing to size for memory. Reach for Playwright specifically when the content only appears after JavaScript runs.Stecken die Daten, die du willst, im rohen HTML oder in einer API-Antwort, die die Seite aufruft, verzichte auf den Browser und nutze requests oder httpx — das ist deutlich leichter, und es gibt beim Speicher nichts zu dimensionieren. Greif gezielt zu Playwright, wenn der Inhalt erst erscheint, nachdem JavaScript gelaufen ist.

Why systemd timers instead of cron?Warum systemd-Timer statt Cron?

Both will run the job. A systemd timer gives you journalctl logs without redirecting output yourself, environment variables handled the normal systemd way, a memory limit via MemoryMax, and Persistent=true to catch a run the machine missed while it was down for something else.Beide erledigen den Job. Ein systemd-Timer gibt dir journalctl-Logs, ohne die Ausgabe selbst umleiten zu müssen, Umgebungsvariablen auf die übliche systemd-Art, ein Speicherlimit über MemoryMax und Persistent=true, um einen Lauf nachzuholen, den die Maschine verpasst hat, weil sie aus einem anderen Grund unten war.

What actually gets a scraper IP-banned?Was führt bei einem Scraper tatsächlich zum IP-Bann?

Almost always request rate, not the fact of scraping itself. A fixed delay between requests to the same host, a real identifying User-Agent, respecting robots.txt, and caching so you never re-fetch a page you already have will keep the overwhelming majority of scraping jobs running indefinitely without anyone noticing or caring.Fast immer die Request-Rate, nicht die Tatsache des Scrapens an sich. Eine feste Verzögerung zwischen Requests an denselben Host, ein echter, identifizierender User-Agent, das Einhalten von robots.txt und Caching, damit du nie eine Seite erneut fetchst, die du schon hast, halten die überwältigende Mehrheit der Scraping-Jobs auf unbestimmte Zeit am Laufen, ohne dass es jemandem auffällt oder jemanden stört.

Ready to order?Bereit zu bestellen?

Prices are final totals; no VAT is shown (§19 UStG). Need something the shop does not list? Email us for a written offer.Alle Preise sind Endpreise ohne ausgewiesene USt. (§19 UStG). Du brauchst etwas, das nicht im Shop steht? Schreib uns für ein schriftliches Angebot.

Order now →Jetzt bestellen → Request a custom configIndividuelle Konfiguration anfragen