Detectar scrapers en logs: la pista de los cero assets
En pocas palabras: El scraper se detectó en 50 segundos porque sus requests no descargaban ningún asset: de 187 peticiones muestreadas, ninguna pedía CSS, JavaScript ni favicon, y 155 venían del mismo lugar. Ese patrón, junto al salto de 3.600 a 18.000 requests por hora a la 01:00 UTC, delató al bot.
Un equipo de desarrollo detectó un scraper en sus logs de servidor en apenas 50 segundos de análisis: cuatro alarmas nocturnas, un salto de 3.600 a 18.000 requests por hora y una pista definitiva, cero peticiones de assets entre 187 requests muestreadas. Ni un CSS, ni un JavaScript, ni un favicon.
Detectar scrapers en logs de servidor es la práctica de analizar los registros de acceso de un sitio web para identificar clientes automatizados que extraen contenido haciéndose pasar por navegadores. Un scraper es un programa que recorre URLs de forma sistemática para copiar datos, y deja huellas medibles en cualquier access.log: volumen anómalo, direcciones IP concentradas en un mismo rango y sesiones sin una sola descarga de CSS, JavaScript o imágenes.
En 30 segundos
- El pico fue de 5x en dos servicios a la vez: prod-api pasó de 3.600 a 18.000 invocaciones por hora y prod-web de 3.600 a 17.000, ambas a la 01:00 UTC, según el relato del propio equipo.
- 155 de 187 requests venían del mismo lugar: el rango de VPS de un proveedor de hosting, con las peticiones distribuidas en ocho o más direcciones IP geolocalizadas en un solo país.
- Los user agents eran Chrome “de verdad”: versiones de escritorio rotando entre cinco builds mayores más una de macOS, sin identificarse nunca como bot.
- Cero assets pedidos: ni un CSS, ni un chunk de JS, ni una fuente, ni un favicon. Un navegador real no puede renderizar sin eso.
- La cache no resuelve el problema: las dos familias de URLs atacadas tienen cientos de miles de IDs distintos, así que cada request era una visita única.
¿Qué significa que un visitante tenga cero peticiones de assets?
Significa que ese cliente no es un navegador humano. Para renderizar cualquier página, un Chrome real descarga decenas de archivos: hojas de estilo, JavaScript, imágenes y hasta el favicon. Si en tus logs aparece HTML puro sin una sola petición de .css, .js o .png, lo que tenés del otro lado es un parser que lee el HTML, extrae lo que busca y descarta el resto.
Cero. Ni un favicon.
La matemática es grosera: una sesión humana típica no puede navegar sin generar peticiones de assets, mientras que este cliente encadenó 187 requests sin pedir un solo archivo estático, a razón de unas cuatro páginas por segundo (que no es poco). Como señalan en su crónica del incidente, los assets son la señal que el scraper olvidó falsificar, y verificarlos es gratis porque ya están en tus logs.
¿Cómo se disfrazan los scrapers para parecer usuarios reales?
Rotan user agents de navegadores reales porque los user agents de bots hace años que figuran en cualquier lista de bloqueo. En este caso, el cliente alternaba user agents de Chrome de escritorio entre cinco versiones mayores, sumaba una build de macOS y jamás se identificó como bot. Eso es una decisión deliberada: alguien escribió código específico para parecer “humano”. Esto se conecta con lo que analizamos en elegir el cloud hosting adecuado.
Ojo con esto: el disfraz tiene una grieta estructural. Simular headers es trivial, pero simular el comportamiento completo de un navegador cuesta CPU y ancho de banda, y la mayoría de los scrapers económicos no lo hace. Por eso el user agent rotado es intencional pero insuficiente: cubre el campo que el atacante controla y deja expuesto el que no. Y ojo con la excepción: un bot verificado como Googlebot se identifica con user agent propio y rangos de IP publicados, exactamente lo contrario de un UA genérico de Chrome saliendo de un rango de VPS.
¿Cómo detectar scrapers en logs de servidor paso a paso?
Con cuatro filtros aplicados en orden llegás al diagnóstico sin herramientas pagas. El caso se resolvió mirando 50 segundos de tráfico en vivo, y el patrón saltó solo:
- Filtrá por volumen anómalo: buscá clientes o redes con 5x o más tráfico sobre su línea base horaria.
- Agrupá por IP, ASN y geolocalización: 155 de las 187 requests venían del rango de VPS de un mismo proveedor, desde ocho o más direcciones.
- Descartá assets con un grep negativo: si no hay .css, .js, .png, .ico ni .woff en el conjunto, no hay navegador de por medio.
- Buscá patrones en las URLs: acá el cliente recorría dos familias de URLs en orden de ID, como quien pasa páginas de un índice.
En un servidor con Nginx o Apache, algo tan simple como esto te da el ranking de IPs que solo piden HTML:
grep -vE '\.(css|js|png|jpe?g|gif|svg|ico|woff2?)' access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head Lo explicamos a fondo en diagnosticar fallos en tu hosting.
Si preferís interfaz visual, GoAccess parsea el access.log en la terminal y te arma el ranking de IPs sin costo. Ahora bien, el comando es el punto de partida, no la sentencia: un proxy corporativo o un monitor de uptime también piden poco estático, y un scraper con headless browser completo sí descarga assets, así que los logs solos no alcanzan para ese perfil. Ahí entra el fingerprinting o un WAF conductual.
¿Por qué el deploy reciente no causó el spike de tráfico?
Porque un deploy cambia latencias y errores, no volumen. Saltan cuatro alarmas a la madrugada, abrís el historial de despliegues convencido de que el último release rompió todo, perdés diez minutos leyendo diffs que no tienen nada que ver y, mientras tanto, el origen sigue recibiendo cuatro páginas por segundo de alguien que nunca va a renderizar nada (spoiler: no era el código).
La pregunta barata que cierra el tema: ¿cambió el volumen total de requests?
Había cambiado. Ambos workers saltaron 5x en el mismo minuto y se quedaron ahí, algo que ningún despliegue produce. La base de datos confirmó el diagnóstico: las lecturas del primario de D1 pasaron de un rango de 15.000 a 28.000 por hora a un sostenido de 37.000 a 46.000, por encima del techo de unas 36.000 lecturas por hora que soporta un primario single-threaded haciendo diez queries por segundo. Todo lo demás, los rechazos, la réplica colapsada y las métricas vacías, era una sola cola atorándose.
¿Cómo proteger mi servidor contra scrapers con rate limiting?
Limitá por red de origen, no por user agent. El user agent lo controla el atacante (ya vimos que rotó cinco versiones de Chrome sin despeinarse); la ruta de red por la que llega el tráfico no. Las primeras tres ideas en la sala fueron todas variantes de cache: TTL más largo, más cache en el edge, stale-while-revalidate. Ninguna sirve contra enumeración, porque una cache que nunca recibe un segundo request es solo un hop extra. Lo que funciona, ordenado por velocidad de implementación: Más contexto en tendencias actuales en infraestructura cloud.
| Defensa | Cómo funciona | Cuándo conviene | Trampa |
|---|---|---|---|
| Rate limit por ASN o prefijo IP | Limita requests por red de origen, no por header | Enumeración masiva desde VPS de hosting | Puede rozar a usuarios legítimos detrás de NAT corporativa |
| Servir familias enumerables barato | Respuesta estática o precalculada, sin lectura de base de datos por request | Familias con cientos de miles de IDs | Solo si el contenido no exige datos frescos |
| Bots verificados | Confía en listas de bots conocidos y validados | Separar bots buenos de malos | En el CDN del caso es feature enterprise; en planes menores no existe |
| Cache tradicional | Guarda respuestas para reutilizarlas | Tráfico repetido sobre mismas URLs | Inútil contra enumeración: cada URL se pide una sola vez |

Acá viene lo bueno: la tercera opción tiene letra chica. El campo de bots verificados del CDN que usaban es exclusivo del plan enterprise, lo alcanzaron a pedir y no pudieron tenerlo, y saberlo de antemano les habría ahorrado veinte minutos. Para quien administra el servidor directo, fail2ban con un filtro que cuente requests a HTML por minuto, más iptables o nftables para el baneo, cubre el caso sin gastar un peso. Y si tu sitio corre en un hosting local como donweb.com, preguntale al soporte qué reglas de filtrado o WAF incluye tu plan antes de descubrirlo a las 3 de la mañana. Sobre la legalidad: bloquear tráfico en tu propia infraestructura es una decisión técnica tuya; los matices legales dependen de cada país y de qué datos se extraen.
¿Cómo evitar falsas alarmas en tu sistema de monitoreo?
Separa las ventanas temporales según lo que medís. Una de las cuatro alarmas de esa noche volvió a dispararse a la mañana siguiente con severidad máxima, por un incidente resuelto desde la tarde anterior: calculaba su tasa sobre una ventana rodante de 24 horas, y el burst seguía adentro aunque el problema llevaba horas muerto.
Una tasa necesita ventana ancha. Un pico necesita gate de recencia: si el incidente se resolvió hace menos de una hora, no re-alertar. Si solo tenés una ventana para todo, terminás siendo alertado por tus propias correcciones, y el día que dejás de confiar en la alarma es justo el día que era real.
Caso de estudio: el diagnóstico completo en 50 segundos de logs
Cuatro alarmas, una causa, y la causa no era nada de lo que el equipo hubiera deployeado. El flujo quedó así: chequeo de volumen (salto de 5x simétrico en dos workers), descarte del deploy, muestreo de 50 segundos de tráfico en vivo, agrupación por origen y lectura del patrón de assets. Veredicto: nuestro “visitante” más prolífico del mes era un scraper enumerando dos familias de URLs en orden de ID desde un rango de VPS, con user agents de Chrome rotativos y cero archivos estáticos pedidos.
Queda una lección de instrumentación que vale oro: tail del proceso que realmente recibe el tráfico. Su worker de API veía las requests reenviadas por el worker de frontend, sin el user agent original; observar ese proceso habría mostrado una pared de requests internas idénticas y nada más. La huella del cliente solo existe en la puerta de entrada verdadera. Nombrá quién recibe el tráfico primero, después tail. Ya lo cubrimos antes en el rol de los DNS autoritativos.
Detalle final con ironía: el autor reconoce que su equipo también scrapea avisos laborales, con presupuesto estricto y solo de fuentes que lo permiten, y que por eso reconoce la forma de un cliente que no lee tu CSS. ¿Quién mejor para cazar a uno de los suyos?
Preguntas Frecuentes
¿Cómo sé si me están scrapeando mi sitio?
Mirá tres señales en tus logs: un salto de volumen de 5x o más sobre tu línea base, requests concentradas en pocos rangos de IP o en el ASN de un proveedor de hosting, y sesiones que piden HTML sin descargar ningún asset. Si además las URLs avanzan en orden secuencial, tenés un enumerator, no un usuario.
¿Qué patrones en los logs delatan a un scraper frente a un humano?
Los tres más confiables son cero peticiones de assets en sesiones largas, cadencia mecánica constante (en este caso fueron cuatro páginas por segundo sin pausas) y recorrido ordenado de URLs en vez de navegación errática. Un user agent de Chrome no compensa nada de eso: el comportamiento pesa más que el header.
¿Sirve de algo bloquear por user agent?
Poco. El user agent es un string que el cliente envía y puede cambiar a voluntad; el atacante de este caso rotaba cinco versiones de Chrome de escritorio más una de macOS. Bloqueá por ASN o prefijo de red, que es lo único que el cliente no puede falsificar con facilidad.
¿La cache ayuda a absorber un ataque de scraping?
No cuando hay enumeración. Las dos familias atacadas tenían cientos de miles de IDs distintos, así que cada request era una URL nueva vista por primera vez y ninguna entrada de cache recibía un segundo hit. Ahí la cache solo suma un hop; lo que funciona es servir esas URLs sin leer la base de datos.
¿Por qué me llegó una alarma de un incidente ya resuelto?
Probablemente tu alerta usa una ventana rodante de 24 horas: el burst viejo sigue dentro de la ventana y la tasa promedio sigue alta aunque el problema esté cerrado. Agregá un gate de recencia que suprima re-alertas de incidentes resueltos hace menos de una hora.
Conclusión
Lo que dejó este caso no es una técnica nueva, es una prioridad: antes de abrir diffs, dashboards o cotizar herramientas, fijate si el volumen cambió y si esas requests piden assets. Dos preguntas, respondibles con grep, nombraron al culpable en 50 segundos mientras cuatro alarmas apuntaban a cualquier otra cosa. Si administrás servidores, tres tareas concretas para esta semana: sumá la detección de cero-assets a tu análisis de logs, configurá rate limiting por ASN en lugar de por user agent, y revisá las ventanas temporales de tus alarmas antes de que te despierten por un arreglo tuyo. Los logs ya tenían toda la información. Siempre la tuvieron.
Fuentes
- Zero asset requests was the tell: finding a scraper in 50 seconds of logs – crónica original del incidente (dev.to)
- Cloudflare Docs – documentación de Workers, D1 y analytics de referencia técnica
- OWASP Automated Threats to Web Applications – catálogo de amenazas automatizadas, incluye el scraping (OAT-008)






