El Problema del Web Scraping: Recursos que Se Comen tu Servidor
Playwright es potente. Pero cada instancia de navegador consume 200-500MB de RAM. Multiplica eso por 10 scrapers concurrentes y tu servidor colapsa. Browserless resuelve exactamente esto.
Browserless es un servicio headless optimizado que ejecuta Chromium de forma eficiente, con pool de navegadores, gestión de recursos y APIs HTTP simples. Todo sin levantar una GUI.
La diferencia:
Métrica | Playwright Solo | Browserless + Playwright |
---|---|---|
RAM por instancia | 400MB | ~100MB (pooling) |
Tiempo de startup | 2-3 segundos | ~0.1 segundo (pre-warm) |
Configuración | Manual | Pre-configurado |
Escalabilidad | Lineal (1 proceso = 1 browser) | Pool inteligente |
Cleanup | Manual (memory leaks comunes) | Automático |
En esta guía vas a aprender a configurar Browserless con Docker, conectarlo a Playwright y crear 5 scrapers prácticos listos para producción.
🎯 Qué Aprenderás
- Instalación optimizada con Docker Compose
- Conexión Playwright con código funcional
- 5 ejemplos de scraping ready-to-use
- Integración con n8n para automatización
- Best practices para producción
Por Qué Browserless + Playwright es la Combinación Perfecta
Ventajas sobre Playwright Solo
- Menos recursos: Pool de navegadores pre-calentados, no levantas/cierras browsers constantemente
- APIs HTTP simples: Puedes hacer scraping con curl, no necesitas siempre código
- Optimizado para servidores: Headless puro, sin X11 ni dependencias gráficas
- Rate limiting built-in: Control de concurrencia automático
- Metrics y monitoring: Endpoint /metrics para Prometheus
- Session management: Mantén sesiones entre requests
Ventajas sobre Puppeteer/Selenium
- Playwright API moderna: Auto-waiting, mejor manejo de SPA, más estable
- Multi-browser: Chromium, Firefox, WebKit (Puppeteer solo Chrome)
- Better debugging: Inspector, traces, screenshots automáticos
- Más rápido: 20-30% más rápido que Puppeteer en benchmarks
Instalación con Docker (5 Minutos)
Opción 1: Docker Run (Quick Test)
docker run -d \
--name browserless \
-p 3000:3000 \
-e "MAX_CONCURRENT_SESSIONS=10" \
-e "CONNECTION_TIMEOUT=60000" \
browserless/chrome:latest
Verifica que funciona:
curl http://localhost:3000/json/version
Deberías ver respuesta JSON con versión de Chromium.
Opción 2: Docker Compose (Recomendado para Producción)
Crea docker-compose.yml
:
version: '3.8'
services:
browserless:
image: browserless/chrome:latest
container_name: browserless
ports:
- "3000:3000"
environment:
# Concurrencia
MAX_CONCURRENT_SESSIONS: 10
QUEUE_LENGTH: 50
# Timeouts (en ms)
CONNECTION_TIMEOUT: 60000
DEFAULT_LAUNCH_ARGS: '["--disable-dev-shm-usage", "--no-sandbox"]'
# Seguridad
TOKEN: "tu_token_super_secreto"
# Performance
PREBOOT_CHROME: "true"
KEEP_ALIVE: "true"
# Debugging (desactiva en producción)
DEBUG: "*"
volumes:
# Para guardar screenshots/PDFs
- ./downloads:/workspace
restart: unless-stopped
# Límites de recursos
mem_limit: 2g
cpus: 2
Levanta el servicio:
docker-compose up -d
Configuraciones Importantes
MAX_CONCURRENT_SESSIONS: Cuántos navegadores simultáneos. Regla de oro: 200MB RAM por sesión. Con 2GB límite → 10 sesiones máximo.
PREBOOT_CHROME: Pre-calienta navegadores. Reduce latencia de primera request pero usa más RAM idle.
TOKEN: Seguridad básica. Todas las requests deben incluir ?token=tu_token
.
DEFAULT_LAUNCH_ARGS: Argumentos de Chromium. Los esenciales:
--no-sandbox
: Necesario en Docker--disable-dev-shm-usage
: Usa /tmp en vez de /dev/shm (evita crashes en containers con poca memoria compartida)--disable-gpu
: No necesitas GPU en headless
Conectar Playwright con Browserless (Python)
Instalación de Playwright
pip install playwright
playwright install chromium # Solo necesario localmente, no en producción con Browserless
Código Base de Conexión
from playwright.sync_api import sync_playwright
BROWSERLESS_URL = "ws://localhost:3000?token=tu_token_super_secreto"
def scrape_with_browserless(url):
with sync_playwright() as p:
# Conecta a Browserless vía WebSocket
browser = p.chromium.connect_over_cdp(BROWSERLESS_URL)
# Usa contexto para aislar sesiones
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
)
page = context.new_page()
try:
# Navega con timeout
page.goto(url, wait_until='networkidle', timeout=30000)
# Tu scraping aquí
title = page.title()
content = page.content()
return {'title': title, 'html': content}
finally:
# Cleanup crucial
context.close()
browser.close()
# Uso
result = scrape_with_browserless('https://example.com')
print(result['title'])
Versión con Node.js
const { chromium } = require('playwright');
const BROWSERLESS_URL = 'ws://localhost:3000?token=tu_token_super_secreto';
async function scrapeWithBrowserless(url) {
const browser = await chromium.connectOverCDP(BROWSERLESS_URL);
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
try {
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
const title = await page.title();
const content = await page.content();
return { title, content };
} finally {
await context.close();
await browser.close();
}
}
// Uso
scrapeWithBrowserless('https://example.com')
.then(result => console.log(result.title));
5 Ejemplos Prácticos de Scraping
1. Scraping Básico con Selectores
Caso de uso: Extraer precios y títulos de productos.
def scrape_products(url):
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSERLESS_URL)
page = browser.new_page()
page.goto(url)
# Espera a que los productos carguen
page.wait_for_selector('.product-card')
# Extrae todos los productos
products = page.eval_on_selector_all('.product-card', '''
(elements) => elements.map(el => ({
title: el.querySelector('h3')?.textContent,
price: el.querySelector('.price')?.textContent,
image: el.querySelector('img')?.src,
link: el.querySelector('a')?.href
}))
''')
browser.close()
return products
# Uso
products = scrape_products('https://shop.example.com/products')
for p in products:
print(f"{p['title']}: {p['price']}")
2. Login y Scraping Autenticado
Caso de uso: Scraping de panel privado después de login.
def scrape_authenticated(login_url, dashboard_url, username, password):
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSERLESS_URL)
context = browser.new_context()
page = context.new_page()
# 1. Navega a login
page.goto(login_url)
# 2. Rellena formulario
page.fill('input[name="username"]', username)
page.fill('input[name="password"]', password)
# 3. Click en submit y espera navegación
with page.expect_navigation():
page.click('button[type="submit"]')
# 4. Verifica login exitoso
page.wait_for_selector('.dashboard')
# 5. Navega a página privada
page.goto(dashboard_url)
# 6. Extrae datos
data = page.eval_on_selector('.stats', '''
(el) => ({
revenue: el.querySelector('.revenue')?.textContent,
users: el.querySelector('.users')?.textContent,
orders: el.querySelector('.orders')?.textContent
})
''')
# Guarda cookies para próximas ejecuciones
cookies = context.cookies()
browser.close()
return data, cookies
3. Scraping con Paginación Infinita
Caso de uso: Sitios con scroll infinito (Twitter, Instagram, etc).
def scrape_infinite_scroll(url, max_scrolls=10):
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSERLESS_URL)
page = browser.new_page()
page.goto(url)
all_items = []
previous_height = 0
scroll_count = 0
while scroll_count < max_scrolls:
# Extrae items actuales
items = page.eval_on_selector_all('.item', '''
(elements) => elements.map(el => ({
title: el.querySelector('.title')?.textContent,
content: el.querySelector('.content')?.textContent
}))
''')
all_items.extend(items)
# Scroll hasta el final
page.evaluate('window.scrollTo(0, document.body.scrollHeight)')
# Espera a que cargue nuevo contenido
page.wait_for_timeout(2000)
# Verifica si hay nuevo contenido
new_height = page.evaluate('document.body.scrollHeight')
if new_height == previous_height:
# No hay más contenido
break
previous_height = new_height
scroll_count += 1
browser.close()
# Deduplica por título
unique_items = {item['title']: item for item in all_items}.values()
return list(unique_items)
4. Screenshots y PDFs
Caso de uso: Generar capturas o PDFs de páginas web.
def generate_screenshot_and_pdf(url, output_dir='./downloads'):
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSERLESS_URL)
page = browser.new_page()
page.goto(url, wait_until='networkidle')
# Screenshot completo
page.screenshot(
path=f'{output_dir}/screenshot.png',
full_page=True
)
# Screenshot de elemento específico
page.locator('#main-content').screenshot(
path=f'{output_dir}/element.png'
)
# PDF (solo funciona en Chromium headless)
page.pdf(
path=f'{output_dir}/page.pdf',
format='A4',
print_background=True
)
browser.close()
return {
'screenshot': f'{output_dir}/screenshot.png',
'pdf': f'{output_dir}/page.pdf'
}
# Uso
files = generate_screenshot_and_pdf('https://example.com/report')
print(f"Guardados: {files}")
5. Manejo de JavaScript Dinámico
Caso de uso: SPAs con contenido que carga vía JavaScript.
def scrape_spa(url):
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSERLESS_URL)
page = browser.new_page()
# Intercepta requests para acelerar (bloquea imágenes, fonts)
page.route('**/*.{png,jpg,jpeg,gif,svg,woff,woff2}', lambda route: route.abort())
page.goto(url)
# Espera a que el contenido dinámico cargue
page.wait_for_selector('[data-loaded="true"]', timeout=10000)
# O espera a que desaparezca el loader
page.wait_for_selector('.loader', state='hidden')
# O espera condición custom
page.wait_for_function('''
() => {
return window.appReady === true &&
document.querySelectorAll('.item').length > 0
}
''', timeout=15000)
# Extrae datos
data = page.evaluate('''
() => {
// Accede a variables JavaScript de la SPA
return {
state: window.__APP_STATE__,
items: Array.from(document.querySelectorAll('.item')).map(el => ({
id: el.dataset.id,
title: el.textContent
}))
}
}
''')
browser.close()
return data
Integración con n8n para Automatización
Browserless se integra perfectamente con n8n para scraping automatizado. Ya tenemos un tutorial completo: Automatización Web con n8n, Browserless y Playwright.
Workflow típico en n8n:
- Cron node → Ejecuta cada hora
- HTTP Request node → Conecta a Browserless
- Code node → Parsea HTML scrapeado
- Database node → Guarda resultados
- Telegram node → Notifica si hay cambios
Código del HTTP Request node:
Method: POST
URL: http://browserless:3000/content?token={{$env.BROWSERLESS_TOKEN}}
Headers:
Content-Type: application/json
Body:
{
"url": "https://target-site.com",
"waitFor": ".main-content",
"gotoOptions": {
"waitUntil": "networkidle"
}
}
Más workflows en: Guía Completa de n8n v2.
Best Practices para Producción
1. Manejo de Errores Robusto
def scrape_with_retry(url, max_retries=3):
for attempt in range(max_retries):
try:
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSERLESS_URL)
page = browser.new_page()
# Set timeout
page.set_default_timeout(30000)
page.goto(url)
data = page.content()
browser.close()
return data
except TimeoutError:
print(f"Timeout en intento {attempt + 1}")
if attempt < max_retries - 1:
time.sleep(5 * (attempt + 1)) # Exponential backoff
else:
raise
except Exception as e:
print(f"Error: {e}")
if attempt == max_retries - 1:
raise
2. Rate Limiting
import time
from collections import deque
class RateLimiter:
def __init__(self, max_requests, time_window):
self.max_requests = max_requests
self.time_window = time_window
self.requests = deque()
def wait_if_needed(self):
now = time.time()
# Elimina requests antiguas
while self.requests and self.requests[0] < now - self.time_window:
self.requests.popleft()
# Si llegamos al límite, espera
if len(self.requests) >= self.max_requests:
sleep_time = self.time_window - (now - self.requests[0])
time.sleep(sleep_time)
self.requests.popleft()
self.requests.append(now)
# Uso: max 10 requests por minuto
limiter = RateLimiter(max_requests=10, time_window=60)
for url in urls:
limiter.wait_if_needed()
scrape_with_browserless(url)
3. Pooling de Conexiones
class BrowserPool:
def __init__(self, pool_size=5):
self.pool_size = pool_size
self.browsers = []
self._init_pool()
def _init_pool(self):
p = sync_playwright().start()
for _ in range(self.pool_size):
browser = p.chromium.connect_over_cdp(BROWSERLESS_URL)
self.browsers.append(browser)
def get_page(self):
browser = self.browsers.pop(0)
page = browser.new_page()
self.browsers.append(browser) # Rotate
return page
def close_all(self):
for browser in self.browsers:
browser.close()
# Uso
pool = BrowserPool(pool_size=5)
for url in urls:
page = pool.get_page()
page.goto(url)
# ... scraping ...
page.close()
pool.close_all()
4. Monitorización
# Verifica salud de Browserless
def check_browserless_health():
import requests
response = requests.get('http://localhost:3000/metrics')
metrics = response.text
print(f"Browserless metrics:\\n{metrics}")
# Parsea métricas Prometheus
lines = metrics.split('\\n')
for line in lines:
if 'browserless_active_sessions' in line:
active = int(line.split()[-1])
print(f"Sesiones activas: {active}")
# Ejecuta cada 5 minutos con cron
check_browserless_health()
Troubleshooting Común
1. "Target closed" / Connection Lost
Causa: Navegador cerrado prematuramente o timeout.
Solución:
- Aumenta CONNECTION_TIMEOUT en Browserless
- Asegúrate de hacer browser.close() en finally
- Verifica que no excedes MAX_CONCURRENT_SESSIONS
2. Alta Uso de RAM
Causa: Demasiadas sesiones concurrentes o memory leaks.
Solución:
- Reduce MAX_CONCURRENT_SESSIONS
- Implementa pooling de conexiones
- Usa page.close() después de cada scraping
- Activa PREBOOT_CHROME=false si no necesitas velocidad
3. Scraping Detectado / Bloqueado
Causa: User agent, falta de cookies, comportamiento no-humano.
Solución:
- Usa user_agent real y variado
- Implementa delays random entre acciones
- Usa stealth plugin: playwright-stealth
- Rota IPs si es necesario (proxies)
Conclusión: Scraping Eficiente y Escalable
Browserless + Playwright es la combinación definitiva para scraping en producción. Menos recursos, más estabilidad, mejor rendimiento.
Recapitulación:
- ✅ 60-70% menos uso de RAM vs Playwright solo
- ✅ Tiempo de startup reducido a ~0.1s (vs 2-3s)
- ✅ Pool de navegadores automático
- ✅ APIs HTTP para scraping sin código
- ✅ Integración perfecta con n8n para automatización
Siguiente paso: Instala Browserless, copia uno de los 5 ejemplos, adáptalo a tu caso de uso. En 15 minutos tendrás scraping en producción.
🚀 Artículos Relacionados
Sobre este tutorial: Todos los ejemplos están probados y funcionan en producción. Código basado en Playwright 1.40+ y Browserless latest. Última actualización: Octubre 2025.
Preguntas Frecuentes sobre Browserless y Playwright
¿Browserless es gratis?
Browserless tiene versión open source gratuita (self-hosted con Docker). También ofrecen servicio cloud con free tier limitado. Para homelab/producción propia: completamente gratis con Docker.
¿Cuánta RAM necesita Browserless?
Cada navegador consume ~100-200MB con pooling activado (vs 400MB+ con Playwright solo). Con 2GB RAM puedes ejecutar 10 sesiones concurrentes confortablemente.
¿Browserless funciona con Puppeteer?
Sí, Browserless soporta tanto Playwright como Puppeteer. Usa el mismo endpoint pero cambiando la librería de conexión. API compatible con ambos.
¿Puedo usar Browserless sin Docker?
Técnicamente sí (instalación npm), pero Docker es altamente recomendado. Simplifica dependencias, aislamiento y configuración. Con Docker: 1 comando y funciona.
¿Browserless detecta bots?
Browserless + Playwright es menos detectable que Selenium. Usa navegador real (Chromium). Para máxima evasión: combina con playwright-stealth plugin y user agents rotatorios.
¿Cómo escalo Browserless?
Múltiples contenedores Browserless detrás de load balancer. Cada contenedor maneja N sesiones. Escala horizontal: 3 containers × 10 sesiones = 30 scrapers concurrentes.