← Volver al blog

5 errores en el middleware de proxy de Scrapy que desperdician el tráfico de proxies

Cinco errores comunes en el middleware de Scrapy que hacen que el tráfico de proxy se desperdicie y que el parser reciba bloqueos. Con ejemplos de código y soluciones listas.

📅29 de septiembre de 2026

¿El parser en Scrapy falla por tiempo de espera, el grupo de proxies se consume mucho más rápido de lo esperado y los registros están llenos de respuestas 407 y 403? ¿Te suena familiar? En el 90% de los casos, el problema no está en los proxies en sí, sino en cómo se ha escrito el DownloaderMiddleware. Analizamos cinco de los errores más comunes en el middleware que convierten el tráfico costoso en solicitudes basura y mostramos cómo corregirlos con código.

Error 1: rotación primitiva sin tener en cuenta el estado del proxy

La construcción más común que se puede encontrar en tutoriales es random.choice(PROXY_LIST) dentro de process_request. El problema es que tal rotación no sabe qué proxy acaba de ser baneado y cuál sigue activo. Como resultado, el parser continúa enviando solicitudes a través de una IP ya bloqueada, recibe 403/429, hace un reintento y vuelve a elegir la misma dirección, porque la selección es aleatoria y no excluye los nodos "malos".

El enfoque correcto es llevar el estado de cada proxy: la cantidad de solicitudes exitosas, la cantidad de errores, el tiempo del último uso. Aquí hay una versión mínima funcional:

import random
import time

class ProxyPool:
    def __init__(self, proxies):
        self.proxies = {p: {"fails": 0, "last_used": 0, "banned_until": 0} for p in proxies}

    def get_proxy(self):
        now = time.time()
        available = [
            p for p, state in self.proxies.items()
            if state["banned_until"] < now
        ]
        if not available:
            # si todos están baneados, restablecemos el ban más "antiguo"
            available = list(self.proxies.keys())
        return random.choice(available)

    def mark_fail(self, proxy, cooldown=300):
        self.proxies[proxy]["fails"] += 1
        self.proxies[proxy]["banned_until"] = time.time() + cooldown

    def mark_success(self, proxy):
        self.proxies[proxy]["fails"] = 0
        self.proxies[proxy]["last_used"] = time.time()

Este pool excluye las IP baneadas durante el tiempo de "enfriamiento" (cooldown) y las devuelve a circulación más tarde. Esto ya reduce el consumo de tráfico drásticamente, porque no estás golpeando el mismo nodo bloqueado repetidamente.

Error 2: manejo incorrecto de reintentos y códigos de estado

El segundo error típico es usar el RetryMiddleware estándar de Scrapy sin modificaciones. Por defecto, reintenta la solicitud en el mismo proxy que falló, a menos que hayas introducido explícitamente un cambio de proxy en process_exception. Como resultado, obtenemos la clásica situación: 3 reintentos, 3 baneos, la solicitud sigue fallando y el tráfico ya se ha gastado.

El segundo punto es que no todos los códigos de estado deben ser reintentados de la misma manera. 429 (Demasiadas solicitudes) requiere una pausa y un cambio de IP, 403 generalmente significa un ban de un proxy específico (se necesita un cambio inmediato), y 5xx a menudo es un problema temporal del lado del servidor, se puede reintentar en el mismo proxy. Si agrupas todo, el middleware quema los proxies de manera demasiado agresiva o espera demasiado donde se necesitaba cambiar IP de inmediato.

class SmartRetryMiddleware:
    def __init__(self, pool):
        self.pool = pool

    def process_response(self, request, response, spider):
        proxy = request.meta.get("proxy")
        if response.status in (403, 407):
            if proxy:
                self.pool.mark_fail(proxy, cooldown=600)
            new_request = request.copy()
            new_request.meta["proxy"] = self.pool.get_proxy()
            new_request.dont_filter = True
            return new_request

        if response.status == 429:
            if proxy:
                self.pool.mark_fail(proxy, cooldown=120)
            new_request = request.copy()
            new_request.meta["proxy"] = self.pool.get_proxy()
            new_request.dont_filter = True
            return new_request

        if proxy:
            self.pool.mark_success(proxy)
        return response

Aquí es importante diferenciar el cooldown según el tipo de error: un ban severo (403/407) — pausa larga, límite de frecuencia (429) — pausa corta. Esto ahorra decenas de por ciento de tráfico en ejecuciones largas.

Error 3: falta de sesiones pegajosas para sitios con autorización

Si el parser trabaja con un sitio que tiene inicio de sesión, carrito, paginación con estado guardado o captcha con verificación por IP, cambiar de proxy en cada solicitud rompe la sesión. El sitio ve que la solicitud nº 1 llegó desde una IP, y la solicitud nº 2 (dentro de la misma cookie-sesión) — desde otra, y esto activa la protección contra bots instantáneamente, incluso si ambas IP son "limpias".

La solución es vincular un proxy a una sesión lógica (por ejemplo, a una cuenta específica o a una cadena de solicitudes dentro de un mismo dominio) durante un tiempo fijo, en lugar de cambiarlo en cada solicitud. Esto se llama sesión pegajosa.

class StickySessionMiddleware:
    def __init__(self, pool, ttl=600):
        self.pool = pool
        self.ttl = ttl
        self.sessions = {}  # session_id -> (proxy, expires_at)

    def process_request(self, request, spider):
        session_id = request.meta.get("session_id")
        if not session_id:
            return

        now = time.time()
        session = self.sessions.get(session_id)

        if session and session[1] > now:
            request.meta["proxy"] = session[0]
        else:
            proxy = self.pool.get_proxy()
            self.sessions[session_id] = (proxy, now + self.ttl)
            request.meta["proxy"] = proxy

Para tareas donde se necesita estabilidad de IP durante toda la sesión — autorización, trabajo con un panel personal, formularios de varios pasos — son más adecuados proxies residenciales con soporte para sesiones: permiten mantener la misma IP de salida durante varios minutos u horas, y luego cambiarla de manera controlada, en lugar de aleatoriamente en cada solicitud.

Error 4: transmisión incorrecta de la autorización del proxy

El cuarto error es técnico, pero se encuentra en casi cada segundo proyecto. Los desarrolladores transmiten el nombre de usuario y la contraseña del proxy directamente en la URL del tipo http://user:pass@ip:port a través de request.meta["proxy"]. Esto funciona en la mayoría de los casos, pero al usar proxies a través de un túnel HTTPS o al trabajar con algunos proveedores, este método de autorización no es procesado correctamente por el estándar HttpProxyMiddleware, y las solicitudes fallan con 407 Proxy Authentication Required, aunque las credenciales sean correctas.

Un método más confiable es transmitir el encabezado Proxy-Authorization explícitamente, codificado en base64:

import base64

class ProxyAuthMiddleware:
    def process_request(self, request, spider):
        proxy = request.meta.get("proxy")
        if not proxy:
            return

        # proxy sin credenciales en la URL
        request.meta["proxy"] = proxy

        user = request.meta.get("proxy_user")
        password = request.meta.get("proxy_pass")
        if user and password:
            credentials = f"{user}:{password}"
            encoded = base64.b64encode(credentials.encode()).decode()
            request.headers["Proxy-Authorization"] = f"Basic {encoded}"

Este enfoque funciona de manera más estable al escalar a cientos de solicitudes paralelas y no depende de cómo la versión específica de la biblioteca analiza las URL con credenciales incrustadas. Esto es especialmente crítico al trabajar con proxies móviles, donde la autenticación a menudo está vinculada a una lista blanca de IP o a una verificación estricta de encabezados.

Error 5: falta de monitoreo y registro de baneos

El último y, posiblemente, el error más costoso en consecuencias es la falta de registro de qué proxies son baneados, con qué frecuencia y en qué dominios. Sin estos datos, es imposible entender qué está consumiendo el tráfico: si el pool de proxies se ha agotado en un sitio específico o si el problema está en el parser mismo (solicitudes demasiado frecuentes, falta de retrasos, encabezados sospechosos).

El conjunto mínimo de métricas que vale la pena registrar en el middleware:

  • Cantidad de solicitudes por cada proxy durante la sesión de parsing
  • Cantidad y códigos de errores (403, 407, 429, 5xx) por cada proxy
  • Tiempo de vida del proxy hasta el primer ban
  • Dominios donde ocurren baneos con más frecuencia
import logging
import json

logger = logging.getLogger("proxy_stats")

class ProxyStatsMiddleware:
    def __init__(self):
        self.stats = {}

    def process_response(self, request, response, spider):
        proxy = request.meta.get("proxy", "unknown")
        domain = request.url.split("/")[2]
        key = f"{proxy}|{domain}"

        entry = self.stats.setdefault(key, {"requests": 0, "errors": 0})
        entry["requests"] += 1
        if response.status in (403, 407, 429):
            entry["errors"] += 1

        if entry["requests"] % 50 == 0:
            logger.info(json.dumps(self.stats))

        return response

Sin tales estadísticas, cualquier intento de "optimizar" el middleware se reduce a conjeturas. Con ellas, puedes ver claramente: si, por ejemplo, un dominio específico banea el 80% del pool de proxies en las primeras 10 solicitudes, el problema no está en los proxies, sino en el patrón de solicitudes (sin rotación de User-Agent, frecuencia demasiado alta, falta de retrasos entre solicitudes).

Ejemplo funcional de middleware completo

Reunimos todo en settings.py: el orden del middleware es crítico, porque depende de qué orden se aplican las verificaciones:

DOWNLOADER_MIDDLEWARES = {
    "myproject.middlewares.ProxyAuthMiddleware": 350,
    "myproject.middlewares.StickySessionMiddleware": 400,
    "myproject.middlewares.SmartRetryMiddleware": 550,
    "myproject.middlewares.ProxyStatsMiddleware": 900,
}

RETRY_ENABLED = False  # desactivamos el reintento estándar, usamos el nuestro
DOWNLOAD_TIMEOUT = 15
CONCURRENT_REQUESTS_PER_DOMAIN = 8

Presta atención a RETRY_ENABLED = False — esto es fundamental, de lo contrario, el mecanismo de reintento integrado de Scrapy entrará en conflicto con tu lógica de cambio de proxy, y las solicitudes comenzarán a duplicarse o reintentarse dos veces. También es importante limitar CONCURRENT_REQUESTS_PER_DOMAIN — una paralelización demasiado alta en un dominio, incluso con rotación de proxies, parece sospechosa para los sistemas anti-bots.

Qué tipo de proxy elegir para Scrapy

El middleware resuelve la mitad del problema, la otra mitad es la elección correcta del pool de proxies para la tarea. A continuación, una comparación de escenarios típicos de parsing.

Tipo de proxy Cuándo usar Ventajas Desventajas
Proxies de centros de datos Parsing masivo de páginas abiertas sin estricta protección anti-bots Alta velocidad, bajo costo de tráfico Fácilmente detectables, a menudo en blacklist
Proxies residenciales Parsing de marketplaces, sitios con protección JS, autorización IP reales, bajo porcentaje de baneos, soporte para sesiones pegajosas Menor velocidad en comparación con DC
Proxies móviles Parsing de versiones móviles de sitios y API con estricta protección anti-bots Máxima confianza por parte de los sitios objetivo El costo de tráfico más alto

Regla práctica: para el parsing de páginas estáticas sin una fuerte protección, son adecuados los proxies de centros de datos baratos en combinación con un middleware competente de este artículo. Si el sitio utiliza Cloudflare, PerimeterX, DataDome o sistemas similares, las IP de centros de datos serán baneadas casi de inmediato, y aquí ya es más rentable pasar directamente a un pool residencial, ahorrando tiempo en la depuración del middleware.

Lista de verificación antes de lanzar el parser en producción

  • El middleware excluye proxies baneados en cooldown, en lugar de elegirlos aleatoriamente
  • La lógica de reintentos diferencia los tipos de errores (403/407 vs 429 vs 5xx) con diferentes cooldowns
  • Para tareas de sesión se utiliza un enlace pegajoso de proxy, en lugar de cambiar en cada solicitud
  • La autorización del proxy se transmite a través del encabezado Proxy-Authorization, no solo a través de la URL
  • Se lleva un registro de estadísticas de baneos por dominios y proxies para diagnosticar problemas
  • El RetryMiddleware estándar de Scrapy está desactivado para no entrar en conflicto con la lógica personalizada
  • La concurrencia se limita a valores razonables, no se establece al máximo

Conclusión

La mayoría de los problemas con el consumo de tráfico de proxies en Scrapy se resuelven no comprando un pool de IP más grande, sino corrigiendo la lógica del middleware: diferenciando errores por tipos, utilizando sesiones pegajosas para escenarios complejos, autorizaciones correctas y monitoreando constantemente los baneos. El código de este artículo se puede tomar como base y adaptar a un proyecto específico: la estructura sigue siendo funcional tanto para pequeños parsers como para instalaciones distribuidas de Scrapy-Cluster.

Si el middleware ya está configurado correctamente y los baneos siguen ocurriendo con demasiada frecuencia, probablemente el problema esté en la calidad del pool de IP. Para el parsing de sitios con protección avanzada contra bots, vale la pena probar proxies residenciales con soporte para sesiones: reducen notablemente el porcentaje de activación de la protección en comparación con las direcciones de centros de datos con la misma lógica de middleware.