Por Que Puppeteer e Playwright São Detectáveis
Se você já tentou fazer scraping com Puppeteer ou Playwright e recebeu um CAPTCHA logo na primeira requisição, não está sozinho. Ferramentas anti-bot como Cloudflare, Datadome e PerimeterX detectam browsers automatizados em segundos — e não é mágica, é ciência.
Os sinais mais explorados por sistemas de detecção incluem:
- navigator.webdriver — o Puppeteer define essa propriedade como
truepor padrão. Qualquer script pode lernavigator.webdrivere identificar que o browser é controlado automaticamente. - Array de plugins inconsistente — browsers reais expõem
navigator.pluginscom entradas como Chrome PDF Viewer. O Puppeteer headless geralmente retorna um array vazio ou com valores inesperados. - Artefatos de iframe do ChromeDriver — variáveis como
window.cdc_adoQpoasnfa76pfcZLmcfl_Arrayou propriedades similares vazam a presença do ChromeDriver. - User-Agent headless — o UA headless contém "HeadlessChrome", um marcador óbvio.
- Permissões de notificação — browsers headless falham de forma diferente em
Notification.permission.
Um único sinal basta. A maioria dos WAFs combina múltiplos sinais em um score de confiança — se o score ultrapassa o limiar, você recebe um desafio ou um block direto.
Puppeteer-Extra com Stealth Plugin: O Que Realmente Corrige
O puppeteer-extra-plugin-stealth é um conjunto de patches de evasão que roda como middleware antes de cada página carregar. Ele não é um único truque — são 10+ sub-plugins que cobrem os sinais mais comuns:
| Sub-plugin | Sinal corrigido | O que faz |
|---|---|---|
| stealth.webdriver | navigator.webdriver | Remove ou redefine para undefined |
| stealth.plugins | navigator.plugins vazio | Injeta plugins realistas no array |
| stealth.languages | navigator.languages | Define idiomas consistentes com o UA |
| stealth.iframe.contentWindow | Artefatos cdc_ | Remove propriedades de ChromeDriver do iframe |
| stealth.user-agent | UA com HeadlessChrome | Substitui por UA de browser normal |
| stealth.permissions | Notification.permission | Simula comportamento de browser real |
| stealth.webgl | WebGL renderer headless | Substitui string do renderer |
A configuração básica é direta:
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/');
// Agora a maioria dos testes passa como "not detected"
await new Promise(r => setTimeout(r, 5000));
await browser.close();
})();
Mas aqui está o problema: stealth resolve fingerprints do browser, não do IP. Se você acessa um site protegido com o mesmo IP de datacenter 100 vezes, o WAF bloqueia pelo IP — não importa quão perfeito seja seu fingerprint.
Combinando Stealth com Proxies Residenciais: A Stack Anti-Detecção Completa
A combinação puppeteer-extra stealth proxy é o que separa scripts amadores de crawlers de produção. O stealth mascara quem é o browser; o proxy residencial mascara de onde vem a requisição.
Proxies residenciais usam IPs de dispositivos reais (ISPs residenciais), então cada requisição parece vir de um usuário legítimo em casa. Proxies de datacenter são baratos, mas os ranges de IP são conhecidos e frequentemente listados em blacklists.
| Tipo de Proxy | Custo | Detectabilidade | Melhor Uso |
|---|---|---|---|
| Datacenter | $$ | Alta — ranges conhecidos | Tarefas sem proteção anti-bot |
| Residencial rotativo | $$$$ | Baixa — IPs de ISP real | Scraping geral, SERP, e-commerce |
| Mobile | $$$$$ | Muito baixa — IPs de operadora | Redes sociais, apps mobile |
Com o ProxyHat, a configuração é simples — as flags de geolocalização e sessão vão diretamente no username:
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
// Proxy residencial com geolocalização para EUA
const PROXY_URL = 'http://user-country-US:PASSWORD@gate.proxyhat.com:8080';
(async () => {
const browser = await puppeteer.launch({
headless: 'new',
args: [
`--proxy-server=${PROXY_URL}`,
'--no-sandbox',
'--disable-setuid-sandbox',
],
});
const page = await browser.newPage();
await page.goto('https://httpbin.org/ip');
const content = await page.content();
console.log(content); // IP residencial dos EUA
await browser.close();
})();
Nota importante: Puppeteer aceita--proxy-serverapenas no formatohost:port. A autenticação deve ser feita via eventoproxyRequiresAuthenticationou com o helperpage.authenticate(). Veja o exemplo completo na seção de rotação.
Autenticação de Proxy no Puppeteer
O Chromium não suporta credenciais diretamente no argumento --proxy-server. Você precisa autenticar via handler de evento:
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
const PROXY_HOST = 'gate.proxyhat.com';
const PROXY_PORT = '8080';
const PROXY_USER = 'user-country-US';
const PROXY_PASS = 'PASSWORD';
(async () => {
const browser = await puppeteer.launch({
headless: 'new',
args: [
`--proxy-server=http://${PROXY_HOST}:${PROXY_PORT}`,
'--no-sandbox',
],
});
const page = await browser.newPage();
// Autenticação do proxy
await page.authenticate({
username: PROXY_USER,
password: PROXY_PASS,
});
await page.goto('https://httpbin.org/ip');
const ip = await page.evaluate(() =>
document.querySelector('pre')?.textContent
);
console.log('IP do proxy:', ip);
await browser.close();
})();
Esse padrão é a base para tudo que vem a seguir — rotação de IP, sticky sessions e geolocalização são controlados alterando o username na chamada page.authenticate().
Avaliadores Customizados: Randomização de Canvas e WebGL por Sessão
O stealth plugin padroniza fingerprints, mas padronizar não é o mesmo que diversificar. Se 100 instâncias do seu crawler geram o mesmo canvas hash, um WAF avançado detecta o padrão. A solução: injetar ruído controlado por sessão.
Randomização de Canvas
Cada sessão injeta um deslocamento aleatório minúsculo nas operações de canvas — invisível para o olho humano, mas suficiente para alterar o hash:
const crypto = require('crypto');
async function injectCanvasNoise(page, sessionId) {
// Gerar seed consistente por sessão, diferente entre sessões
const seed = crypto
.createHash('sha256')
.update(sessionId)
.digest('hex');
const offset = parseInt(seed.slice(0, 8), 16) % 10 - 5; // -5 a +4
await page.evaluateOnNewDocument((offset) => {
const origToDataURL = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function (type) {
const ctx = this.getContext('2d');
if (ctx) {
// Injetar ruído sub-pixel imperceptível
const imgData = ctx.getImageData(
0, 0,
Math.min(this.width, 1),
Math.min(this.height, 1)
);
imgData.data[0] = Math.max(0, Math.min(255, imgData.data[0] + offset));
ctx.putImageData(imgData, 0, 0);
}
return origToDataURL.apply(this, [type]);
};
}, offset);
}
// Uso: antes de page.goto()
await injectCanvasNoise(page, 'session-abc123');
Randomização de WebGL
De forma similar, o renderer e vendor do WebGL podem ser randomizados por sessão:
const GL_RENDERERS = [
'ANGLE (Intel, Intel(R) UHD Graphics 630, OpenGL 4.5)',
'ANGLE (NVIDIA, NVIDIA GeForce GTX 1060, OpenGL 4.6)',
'ANGLE (AMD, AMD Radeon RX 580, OpenGL 4.5)',
'ANGLE (Intel, Intel(R) Iris(R) Xe Graphics, OpenGL 4.6)',
];
const GL_VENDORS = ['Google Inc. (Intel)', 'Google Inc. (NVIDIA)', 'Google Inc. (AMD)'];
async function injectWebGLFingerprint(page, sessionId) {
const idx = crypto
.createHash('sha256')
.update(sessionId)
.digest()
.readUInt8(0) % GL_RENDERERS.length;
await page.evaluateOnNewDocument((renderer, vendor) => {
const getParameter = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function (param) {
// UNMASKED_RENDERER_WEBGL = 0x9246
// UNMASKED_VENDOR_WEBGL = 0x9245
if (param === 0x9246) return renderer;
if (param === 0x9245) return vendor;
return getParameter.call(this, param);
};
}, GL_RENDERERS[idx], GL_VENDORS[idx]);
}
// Uso
await injectWebGLFingerprint(page, 'session-abc123');
Combinados, esses dois evaluators garantem que cada sessão do seu crawler tenha um fingerprint único — mas ainda realista e consistente dentro da própria sessão.
Rotação de Proxy por Browser Context
Uma técnica avançada de Puppeteer anti-detection é usar browser.createIncognitoBrowserContext() para isolar sessões. Cada contexto pode ter seu próprio proxy e fingerprint, enquanto compartilha a mesma instância do Chromium — economizando RAM significativamente em escala.
const puppeteer = require('puppeteer-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
puppeteer.use(StealthPlugin());
const PROXY_HOST = 'gate.proxyhat.com';
const PROXY_PORT = '8080';
const PROXY_PASS = 'PASSWORD';
// Fila de países para rotacionar
const COUNTRIES = ['US', 'DE', 'BR', 'JP', 'GB'];
async function createContextWithProxy(browser, country) {
const context = await browser.createIncognitoBrowserContext();
const page = await context.newPage();
// Cada contexto usa um proxy de país diferente
const username = `user-country-${country}`;
await page.authenticate({
username,
password: PROXY_PASS,
});
// Injetar fingerprint único por contexto
const sessionId = `ctx-${country}-${Date.now()}`;
await injectCanvasNoise(page, sessionId);
await injectWebGLFingerprint(page, sessionId);
return { context, page, country };
}
(async () => {
const browser = await puppeteer.launch({
headless: 'new',
args: [
`--proxy-server=http://${PROXY_HOST}:${PROXY_PORT}`,
'--no-sandbox',
],
});
// Criar 5 contextos isolados com proxies de países diferentes
const sessions = await Promise.all(
COUNTRIES.map(c => createContextWithProxy(browser, c))
);
// Executar tarefas em paralelo
const results = await Promise.allSettled(
sessions.map(async ({ page, country }) => {
await page.goto('https://httpbin.org/ip');
const ip = await page.evaluate(() =>
document.querySelector('pre')?.textContent
);
console.log(`${country}: ${ip}`);
})
);
// Limpeza
for (const { context } of sessions) {
await context.close();
}
await browser.close();
})();
Importante: o argumento--proxy-serveré global para o browser. Para usar proxies diferentes por contexto, você precisa de um proxy rotativo no lado do servidor (como o ProxyHat) que alterna IPs com base no username. Opage.authenticate()com usernames diferentes garante que cada contexto obtenha um IP diferente do pool.
Escalando para Produção: Fleet Containerizada e Browser Pools
Um crawler de produção não é um script — é um sistema. Aqui estão os padrões que funcionam em escala.
1. Pool de Browsers com Reutilização
Criar e destruir instâncias do Chromium a cada requisição é caro. Um pool com aquecimento prévio reduz latência e consumo de CPU:
class BrowserPool {
constructor(maxBrowsers = 5) {
this.maxBrowsers = maxBrowsers;
this.pool = [];
this.waitQueue = [];
}
async init() {
for (let i = 0; i < this.maxBrowsers; i++) {
const browser = await puppeteer.launch({
headless: 'new',
args: [
'--proxy-server=http://gate.proxyhat.com:8080',
'--no-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
],
});
this.pool.push({ browser, inUse: false });
}
}
async acquire() {
const available = this.pool.find(b => !b.inUse);
if (available) {
available.inUse = true;
return available;
}
if (this.pool.length < this.maxBrowsers) {
const browser = await puppeteer.launch({
headless: 'new',
args: ['--proxy-server=http://gate.proxyhat.com:8080', '--no-sandbox'],
});
const entry = { browser, inUse: true };
this.pool.push(entry);
return entry;
}
// Esperar até um slot ficar livre
return new Promise(resolve => this.waitQueue.push(resolve));
}
release(entry) {
entry.inUse = false;
if (this.waitQueue.length > 0) {
const next = this.waitQueue.shift();
entry.inUse = true;
next(entry);
}
}
async destroy() {
await Promise.all(this.pool.map(b => b.browser.close()));
this.pool = [];
}
}
2. Containerização com Docker
Cada worker roda em seu próprio container com recursos limitados. Isso garante isolamento de falhas e escalabilidade horizontal:
# Dockerfile
FROM node:20-slim
# Dependências do Chromium
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/*
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
ENV NODE_ENV=production
# Limitar recursos: 1 CPU, 1GB RAM
CMD ["node", "--max-old-space-size=768", "worker.js"]
3. Orquestração com Docker Compose
# docker-compose.yml
version: '3.8'
services:
scraper-worker:
build: .
deploy:
replicas: 10
resources:
limits:
cpus: '1.0'
memory: 1024M
reservations:
cpus: '0.5'
memory: 512M
environment:
- PROXY_USER_BASE=user-country-US
- PROXY_PASSWORD=PASSWORD
- PROXY_HOST=gate.proxyhat.com
- PROXY_PORT=8080
restart: unless-stopped
4. Métricas Essenciais
Monitore estas métricas para manter a saúde do fleet:
- Taxa de sucesso — % de requisições que retornam 200 sem CAPTCHA. Target: >95%.
- Latência P50/P95 — tempo de carregamento incluindo proxy. Se P95 > 15s, investigue.
- Uso de memória por browser — Chromium pode consumir 300-500MB por aba. Limite abas por instância.
- Taxa de rotação de IP — se um IP é reutilizado mais de 5x no mesmo domínio, aumente a rotação.
5. Sticky Sessions vs Rotação por Requisição
Para login e fluxos multi-step, use sticky sessions que mantêm o mesmo IP por 10-30 minutos:
// Sticky session: o IP permanece o mesmo enquanto o sessionId não mudar
const STICKY_USER = 'user-country-US-session-orderFlow432';
await page.authenticate({
username: STICKY_USER,
password: PROXY_PASS,
});
Para scraping de SERP ou monitoramento de preços onde cada requisição é independente, use rotação por requisição — basta omitir a flag session- ou usar um ID diferente a cada chamada.
Considerações Éticas e Legais
Stealth e proxies são ferramentas poderosas — e com grande poder vem grande responsabilidade. Use essa stack para:
- Scraping legítimo — coletar dados públicos para pesquisa, monitoramento de preços, verificação de SEO.
- QA e testes — simular usuários de diferentes localizações para testar sua própria aplicação.
- Coleta de dados para IA — reunir datasets de treinamento a partir de fontes públicas.
Não use para:
- Violar termos de serviço de plataformas de forma fraudulenta.
- Burlar limites de taxa para ataques de credential stuffing ou carding.
- Enganar sistemas de verificação de identidade.
Respeite robots.txt, implemente rate limiting razoável, e esteja em conformidade com GDPR e CCPA quando processar dados pessoais. Se um site oferece uma API pública, use-a em vez de scraping — é mais confiável e mais rápido.
Key Takeaways
- Puppeteer puro é detectável por sinais como
navigator.webdriver, plugins vazios e artefatos do ChromeDriver — o stealth plugin corrige a maioria desses sinais automaticamente. - Stealth resolve o fingerprint do browser, mas não o IP — proxies residenciais são essenciais para evitar bloqueios baseados em IP.
- Combine
puppeteer-extra-plugin-stealthcom proxies residenciais do ProxyHat para a stack anti-detecção mais robusta. - Randomize canvas e WebGL fingerprints por sessão para evitar detecção por correlação de hash.
- Use
browser.createIncognitoBrowserContext()para isolar sessões com proxies diferentes na mesma instância do Chromium. - Em produção, use browser pools, containerização e métricas para escalar com confiabilidade.
- Stealth é para scraping legítimo — não para fraude.
Pronto para construir crawlers que realmente funcionam em produção? Confira os planos de proxy do ProxyHat e as localizações disponíveis para começar hoje.






