Warum rohes Puppeteer sofort erkannt wird
Wenn du Puppeteer oder Playwright direkt nutzt, verrät dein Browser dutzende Signale, die ihn als Automations-Tool entlarven. Moderne Anti-Bot-Systeme wie Cloudflare, Datadome oder PerimeterX prüfen nicht mehr nur einen einzelnen Fingerabdruck — sie korrelieren dutzende Eigenschaften und werten Verhaltensmuster aus. Ein navigator.webdriver === true reicht bereits für einen Block.
Die häufigsten Erkennungsvektoren bei unbehandelten Puppeteer-Instanzen:
- navigator.webdriver — Chrome setzt diese Eigenschaft automatisch auf
true, wenn das DevTools-Protokoll aktiv ist. - Inkonsistente Plugins/MimeTypes — Ein Headless-Browser meldet oft
[]fürnavigator.plugins, während echter Chrome mindestens 5 Einträge liefert. - iframe-ChromeDriver-Artefakte — Das
cdc_-Property anwindow.documentverrät Chromedriver-basierte Automations-Frameworks. - WebGL-Renderer-Strings — SwiftShader als Renderer ist ein Headless-Indikator.
- Missing Permissions-API — Headless-Browser verhalten sich bei
navigator.permissions.query()anders als echte Browser. - WebDriver-BiDi-Flags — Seit Chrome 112 existieren weitere CDP-basierte Erkennungspunkte.
Das Ergebnis: Dein Request wird stillschweigend blockiert, oder du landest in einer CAPTCHA-Schleife, die den gesamten Crawl invalidiert.
puppeteer-extra und das Stealth-Plugin — Was gepatcht wird
puppeteer-extra ist ein Wrapper um Puppeteer, der ein Plugin-System bereitstellt. Das puppeteer-extra-plugin-stealth bündelt über ein Dutzend Evasion-Module, die die häufigsten Erkennungsvektoren addressieren:
| Stealth-Modul | Was es patcht | Erkennungsvektor |
|---|---|---|
navigator.webdriver | Setzt die Eigenschaft auf undefined | WebDriver-Flag |
chrome.runtime | Injiziert window.chrome.runtime | Fehlende Chrome-API |
plugins/mimeTypes | Fügt realistische Plugin-Einträge hinzu | Leere Plugin-Liste |
iframe.contentWindow | Korrigiert cdc_-Artefakte in iframes | ChromeDriver-Fingerprint |
WebGL-Vendor/Renderer | Überschreibt SwiftShader-Strings | Headless-Renderer |
Permissions-API | Normalisiert navigator.permissions | Abweichendes Permission-Verhalten |
User-Agent | Korrigiert Headless-Kennung im UA-String | Headless-UA |
Media-Codecs | Fügt Codec-Support-Info hinzu | Fehlende Codec-Unterstützung |
Die Grundkonfiguration ist denkbar einfach:
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
(async () => {
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
await page.goto('https://bot.sannysoft.com/');
// Die meisten roten Flaggen sollten jetzt grün sein
await page.screenshot({ path: 'stealth-check.png' });
await browser.close();
})();
Damit bist du bereits einen großen Schritt weiter — aber ohne Residential Proxies fehlt dir die Netzwerk-Ebene. IP-basierte Blockierungen erkennen den Datacenter-IP-Range, unabhängig davon, wie gut dein Browser-Fingerprint ist.
Stealth + Residential Proxies — Der stärkste Anti-Detection-Stack
Anti-Bot-Systeme arbeiten auf zwei Ebenen: Browser-Fingerprint (Client-Seite) und IP-Reputation (Netzwerk-Seite). Wenn dein Browser-Fingerprint perfekt ist, aber die IP aus einem Datacenter-Range stammt, der auf Blacklists steht, wirst du trotzdem blockiert. Die Kombination aus Stealth-Plugin und Residential Proxies schließt genau diese Lücke.
Mit ProxyHat Residential Proxies bekommst du echte ISP-IPs mit Geo-Targeting — das ist der Unterschied zwischen einem Crawl, der nach 50 Requests stirbt, und einem, der Millionen Requests durchhält.
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
// ProxyHat Residential Proxy mit Geo-Targeting (Deutschland)
const PROXY_URL = 'http://user-country-DE:PASSWORD@gate.proxyhat.com:8080';
(async () => {
const browser = await puppeteer.launch({
headless: 'new',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
`--proxy-server=${PROXY_URL}`
]
});
const page = await browser.newPage();
// Authentifizierung für den Proxy
await page.authenticate({
username: 'user-country-DE',
password: 'PASSWORD'
});
await page.goto('https://ipinfo.io/json');
const ipInfo = await page.evaluate(() =>
JSON.parse(document.body.innerText)
);
console.log('Proxy-IP:', ipInfo.ip, 'Land:', ipInfo.country);
await browser.close();
})();
Pro-Tipp: Nutze Sticky Sessions für Login-Flows, damit deine IP während der gesamten Session konstant bleibt. Wechsle zu per-request-Rotation für großflächiges Scraping.
Custom Evaluatoren — Canvas- und WebGL-Fingerprint-Randomisierung
Das Stealth-Plugin patcht viele Signale, aber Canvas- und WebGL-Fingerprints sind hartnäckig. Zwei Puppeteer-Instanzen auf demselben Server erzeugen denselben Canvas-Hash — das ist ein Korrelations-Risiko. Die Lösung: page.evaluateOnNewDocument() injiziert Fingerprint-Randomisierung bevor die Seite lädt.
function createFingerprintSeed() {
// Kleiner, zufälliger Offset — subtil genug, um nicht aufzufallen
const noise = () => (Math.random() - 0.5) * 0.01;
return {
canvasNoise: noise(),
webglVendor: 'Google Inc. (NVIDIA)',
webglRenderer: `ANGLE (NVIDIA, NVIDIA GeForce GTX 1060, OpenGL 4.5)`,
audioNoise: noise()
};
}
async function injectFingerprintRandomization(page, seed) {
await page.evaluateOnNewDocument((s) => {
// Canvas-Fingerprint-Randomisierung
const origToDataURL = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function (...args) {
const ctx = this.getContext('2d');
if (ctx) {
const imgData = ctx.getImageData(0, 0, this.width, this.height);
// Subtiler Noise-Pixel an zufälliger Position
const idx = Math.floor(Math.random() * imgData.data.length / 4) * 4;
imgData.data[idx] = Math.max(0, imgData.data[idx] + Math.round(s.canvasNoise * 255));
ctx.putImageData(imgData, 0, 0);
}
return origToDataURL.apply(this, args);
};
// WebGL-Vendor/Renderer-Überschreibung
const getParameterOrig = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function (param) {
if (param === 37445) return s.webglVendor; // UNMASKED_VENDOR_WEBGL
if (param === 37446) return s.webglRenderer; // UNMASKED_RENDERER_WEBGL
return getParameterOrig.call(this, param);
};
}, seed);
}
// Verwendung pro Session
const seed = createFingerprintSeed();
await injectFingerprintRandomization(page, seed);
Diese Technik erzeugt pro Browser-Context einen einzigartigen Fingerprint, ohne die visuelle Darstellung spürbar zu verändern. Das ist entscheidend: Wenn dein Noise zu offensichtlich ist, erkennen Fingerprinting-Bibliotheken die Manipulation.
Per-Browser-Context Proxy-Rotation
Ab Puppeteer v1.8+ unterstützt Chrome das --proxy-server-Flag nur pro Browser-Instanz, nicht pro Context. Für echte per-Context-Rotation brauchst du mehrere Browser-Instanzen — oder du nutzt einen lokalen Proxy-Manager, der die Rotation übernimmt. Hier ist ein sauberes Pattern mit Browser-Pool und ProxyHat:
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
class BrowserPool {
constructor({ maxBrowsers = 5, proxyConfig }) {
this.maxBrowsers = maxBrowsers;
this.proxyConfig = proxyConfig;
this.pool = [];
this.nextIdx = 0;
}
_buildProxyUrl(sessionId) {
// Per-Browser Sticky Session mit einzigartiger Session-ID
return `http://user-session-${sessionId}:${this.proxyConfig.password}@gate.proxyhat.com:8080`;
}
async init() {
for (let i = 0; i < this.maxBrowsers; i++) {
const sessionId = `pool-${i}-${Date.now()}`;
const proxyUrl = this._buildProxyUrl(sessionId);
const browser = await puppeteer.launch({
headless: 'new',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
`--proxy-server=${proxyUrl}`
]
});
this.pool.push({
browser,
sessionId,
proxyUrl,
inUse: false
});
}
return this;
}
async acquire() {
// Round-Robin-Zuweisung
const entry = this.pool[this.nextIdx % this.pool.length];
this.nextIdx++;
entry.inUse = true;
const page = await entry.browser.newPage();
await page.authenticate({
username: `user-session-${entry.sessionId}`,
password: this.proxyConfig.password
});
return { page, session: entry };
}
async release(session) {
session.inUse = false;
}
async close() {
for (const entry of this.pool) {
await entry.browser.close();
}
this.pool = [];
}
}
// Verwendung
const pool = await new BrowserPool({
maxBrowsers: 5,
proxyConfig: { password: 'PASSWORD' }
}).init();
const { page, session } = await pool.acquire();
await page.goto('https://example.com');
// ... Scraping-Logik ...
await pool.release(session);
Dieses Pattern gibt dir isolierte Sessions mit unterschiedlichen IPs, ohne dass du manuell Browser-Instanzen verwalten musst. Jeder Browser im Pool hat seine eigene Sticky Session, und der Round-Robin-Mechanismus verteilt die Last gleichmäßig.
Sticky Sessions vs. per-Request-Rotation
| Strategie | ProxyHat Username-Format | Beste für | IP-Stabilität |
|---|---|---|---|
| Per-Request-Rotation | user-country-DE | SERP-Scraping, Preisvergleich | Neue IP pro Request |
| Sticky Session (30 Min) | user-session-abc123-country-DE | Login-Flows, Checkout-Prozesse | Konstante IP pro Session |
| Sticky Session + City | user-session-xyz-city-berlin | Lokalisierte E-Commerce-Scraping | Konstante IP + Geo |
Scaling — Containerisierte Browser-Flotten und Ressourcen-Management
Ein einzelner Server schafft etwa 5–8 gleichzeitige Puppeteer-Instanzen, bevor RAM und CPU zum Flaschenhals werden. Für produktionsreife Crawler brauchst du Container-Isolation und horizontale Skalierung.
Docker-Image mit Puppeteer-Extra
FROM node:20-slim
# Chromium + Abhängigkeiten installieren
RUN apt-get update && apt-get install -y \
chromium \
fonts-liberation \
libappindicator3-1 \
libasound2 \
libatk-bridge2.0-0 \
libdrm2 \
libgbm1 \
libgtk-3-0 \
libnspr4 \
libnss3 \
libxss1 \
xdg-utils \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
# Nicht als Root ausführen
RUN groupadd -r pptruser && useradd -r -g pptruser pptruser \
&& mkdir -p /home/pptruser/Downloads \
&& chown -R pptruser:pptruser /app
USER pptruser
CMD ["node", "crawler.js"]
Docker Compose für horizontale Skalierung
version: '3.8'
services:
crawler:
build: .
environment:
- PROXY_PASSWORD=${PROXY_PASSWORD}
- MAX_CONCURRENT_PAGES=4
- CONCURRENCY_PER_INSTANCE=3
deploy:
replicas: 4
resources:
limits:
memory: 2G
cpus: '1.0'
reservations:
memory: 1G
restart: unless-stopped
healthcheck:
test: ["CMD", "node", "-e", "process.exit(0)"]
interval: 30s
timeout: 10s
retries: 3
Ressourcen-Optimierung in der Praxis
- Memory: Setze
--max-old-space-size=1024undpage.setDefaultNavigationTimeout(30000), um Memory Leaks einzudämmen. - Concurrency: Maximal 3–4 Tabs pro Browser-Instanz. Mehr Tabs erhöhen den Fingerprint-Korrelations-Risiko und den RAM-Verbrauch exponentiell.
- Page-Recycling: Schließe Tabs nach Gebrauch mit
page.close()statt neue zu öffnen — das verhindert Memory Leaks. - Graceful Shutdown: Fang SIGTERM/SIGINT ab und schließe den Browser sauber, um Zombie-Prozesse zu vermeiden.
- Health Checks: Überwache den RSS-Speicher des Node-Prozesses. Wenn er über 1,5 GB steigt, starte den Container neu.
Produktionsreife Crawler-Architektur
Für echtes Scaling brauchst du eine Job-Queue (z.B. BullMQ mit Redis), Worker die Browser-Instanzen managen, und einen Orchestrator der die Verteilung übernimmt:
- Producer pusht URLs in die Queue.
- Worker ziehen Jobs, erstellen eine Browser-Instanz mit Proxy, scrapen, und speichern Ergebnisse.
- Orchestrator skaliert Worker basierend auf Queue-Tiefe hoch/runter.
Dieses Pattern lässt sich auf Kubernetes mit HPA (Horizontal Pod Autoscaler) oder auf AWS ECS mit Service Auto Scaling umsetzen. Die Kombination aus ProxyHat-Sticky-Sessions und Container-Isolation gibt dir maximale Zuverlässigkeit.
Ethische Hinweise — Stealth für legitimes Scraping
Anti-Detection-Techniken sind ein mächtiges Werkzeug, aber mit Macht kommt Verantwortung. Stealth ist für legitimes Scraping, nicht für Betrug.
- Respektiere robots.txt — Wenn eine Seite Scraping explizit untersagt, kontaktiere den Betreiber für eine API-Lösung.
- Rate-Limiting — Auch mit Residential Proxies solltest du vernünftige Delays zwischen Requests einbauen.
await page.waitForTimeout(2000 + Math.random() * 3000)simuliert menschliches Verhalten. - GDPR/CCPA — Persönliche Daten dürfen nicht ohne Einwilligung gescraped werden. Das gilt auch für öffentlich zugängliche Profile.
- Terms of Service — Die ToS mancher Plattformen verbieten automatisierten Zugriff. Wenn du dagegen verstößt, riskierst du rechtliche Konsequenzen.
- Kein Credential-Stuffing — Stealth-Techniken dürfen nicht verwendet werden, um Login-Seiten für Account-Takeover-Angriffe zu umgehen.
Die beste Anti-Detection-Strategie ist immer noch: Frag nach einer API. Viele Plattformen bieten offizielle Datenzugänge — oft günstiger als der Aufwand für einen produktionsreifen Stealth-Crawler.
Vergleich: Anti-Detection-Strategien
| Ansatz | Erkennungsrisiko | Komplexität | Kosten | Empfehlung |
|---|---|---|---|---|
| Rohes Puppeteer | Sehr hoch | Niedrig | Keine | Nur für Testumgebungen |
| Puppeteer + Stealth-Plugin | Mittel | Niedrig | Keine | Guter Startpunkt |
| Stealth + Datacenter-Proxy | Mittel-Hoch | Mittel | Niedrig | IP-Reputation bleibt Risiko |
| Stealth + Residential Proxy | Niedrig | Mittel | Mittel | Produktionsreif ✅ |
| Stealth + Residential + Fingerprint-Randomisierung | Sehr niedrig | Hoch | Mittel | Enterprise-Grade ✅ |
Key Takeaways
- Rohes Puppeteer ist sofort erkennbar — navigator.webdriver, fehlende Plugins und ChromeDriver-Artefakte reichen für einen Block.
- puppeteer-extra-plugin-stealth patcht die häufigsten Erkennungsvektoren, reicht allein aber nicht aus.
- Residential Proxies sind Pflicht — Datacenter-IPs haben eine schlechte Reputation, egal wie gut dein Browser-Fingerprint ist.
- Fingerprint-Randomisierung pro Session verhindert Korrelations-Angriffe über mehrere Crawl-Sessions hinweg.
- Per-Browser-Context Proxy-Rotation mit ProxyHat-Sticky-Sessions ist die sauberste Architektur für produktive Crawler.
- Container-Isolation und Browser-Pools sind der Weg von einem Skript zu einem produktionsreifen System.
- Ethik first — Stealth für legitimes Scraping, niemals für Betrug.
Wenn du einen produktionsreifen Scraping-Stack aufbauen willst, starte mit ProxyHat Residential Proxies und dem Browser-Pool-Pattern oben. Die Kombination aus Stealth-Plugin, Fingerprint-Randomisierung und echten ISP-IPs gibt dir die höchste Erfolgsquote bei gleichzeitig sauberer Architektur. Mehr zu Proxy-Standorten und Geo-Targeting findest du auf unserer Locations-Seite.






