Scraping

Reintentos y Backoff: Cómo los Scrapers Convierten Pequeños Fallos en Bloqueos

La mayoría de los bloqueos de scrapers son autoinfligidos. Un bucle de reintentos ingenuo responde a una respuesta limitada con una ráfaga de tráfico, justo cuando un sitio pidió menos.

Chris Collins

Chris Collins

23 de agosto de 2026 · 10 min de lectura

Un scraper recibe una respuesta lenta, así que reintenta. El reintento también falla, así que vuelve a reintentar, de inmediato, y lo mismo hace cada uno de los demás workers que chocaron con el mismo muro en el mismo momento. En cuestión de segundos has enviado una ráfaga de tráfico a un sitio que ya te estaba diciendo que quería menos, y lo que empezó como una limitación temporal es ahora un bloqueo duro sobre todas las direcciones implicadas. El sitio no escaló. Tu bucle de reintentos sí lo hizo.

Esta es la forma más común en que un scraper bien construido se hace daño a sí mismo, y es totalmente evitable. La lógica de reintentos es un mecanismo de descarga de carga, no un mecanismo de persistencia, y la diferencia entre esas dos ideas es la diferencia entre un trabajo que se degrada con elegancia y uno que termina bloqueado. La higiene general se trata en avoiding blocks; esto es la mecánica del propio camino de reintento.

Por qué los reintentos ingenuos empeoran las cosas

Tres dinámicas se combinan, y lo hacen a la vez.

La primera es que los reintentos añaden carga justo cuando la carga es el problema. Un 429 o una respuesta lenta es una petición para que envíes menos, y responder a ello con más peticiones invierte la señal. La segunda es la sincronización. Los workers que fallan en el mismo momento y esperan el mismo intervalo fijo también reintentarán en el mismo momento, así que en lugar de dispersarse llegan como una ráfaga coordinada, y cada ronda de esa ráfaga vuelve a sincronizar la siguiente. La tercera es que los reintentos normalmente se cuentan en tu contra por dirección, así que machacar un objetivo desde la misma IP mueve esa dirección de limitada a marcada, lo cual es un problema de reputación que sobrevive al incidente y sigue a la dirección hasta tu siguiente trabajo.

Juntando todo, un bucle ingenuo toma una condición recuperable y produce exactamente la forma de tráfico que los sistemas antibot están diseñados para detectar. El mismo fallo, gestionado con contención, se habría resuelto por sí solo.

Clasifica antes de reintentar

La primera regla es que no todo fallo merece un reintento, y los que sí lo merecen merecen reintentos distintos. Clasifica las respuestas en tres grupos.

Algunos fallos son transitorios y merece la pena reintentarlos: reinicios de conexión, tiempos de espera agotados, 502, 503, 504 y 429. Estos representan un sistema que está momentáneamente incapaz en lugar de indispuesto, y el 429 en particular es una instrucción explícita sobre el ritmo más que un rechazo. Algunos son terminales y nunca deben reintentarse: 404, 400, 401, 403 que persiste en distintas direcciones, y una página que se analizó correctamente pero no contenía nada de lo que buscabas. Reintentar estos quema ancho de banda y reputación por un resultado que no puede cambiar, y un fallo de parseo es un error de código que un reintento reproducirá fielmente para siempre.

El tercer grupo es el peligroso: respuestas que parecen exitosas y no lo son. Una página de desafío, un resultado genérico o vacío, un listado truncado, o una redirección a una landing page pueden llegar todas con un estado 200, y un scraper que confía solo en los códigos de estado las registrará alegremente como datos. Valida el cuerpo antes de contar una respuesta como éxito, que es la sustancia de detecting blocked or fake content. Un bloqueo blando es un fallo reintentable, pero solo si notas que es un fallo.

Backoff exponencial, y por qué el jitter no es opcional

Para el grupo reintentable, el retraso entre intentos debe crecer, y la forma estándar es exponencial: espera un segundo, luego dos, luego cuatro, luego ocho. El crecimiento importa porque da a un objetivo con dificultades progresivamente más margen en lugar de un tambor constante.

Pero el backoff exponencial por sí solo no basta, y esta es la parte que la gente se salta. Si cien workers fallan a la vez y todos esperan exactamente un segundo, reintentarán juntos un segundo después. El backoff creció, pero la ráfaga sobrevivió, y simplemente has movido el mismo pico en la línea de tiempo. La solución es el jitter: aleatorizar cada retraso en lugar de usar directamente el valor calculado. El full jitter, es decir, una espera aleatoria elegida entre cero y el techo actual, transforma un fallo sincronizado en una distribución suave de reintentos. Es un cambio de dos líneas y es lo más eficaz de todo este artículo.

Con esto van dos reglas más. Respeta Retry-After cuando un sitio lo envía, porque esa cabecera es el objetivo diciéndote exactamente cuánto esperar, e ignorarla en favor de tu propio calendario es a la vez descortés y contraproducente. Y limita tanto el retraso como el número de intentos, porque una petición que ha fallado cinco veces no va a tener éxito en la sexta, y un reintento sin límite es solo una forma lenta de no rendirse nunca ante algo que ya está perdido.

import random, time
import requests

PROXY = "http://customer-USERNAME-country-us:PASSWORD@p.shifter.io:443"
PROXIES = {"http": PROXY, "https": PROXY}

TRANSIENT = {429, 502, 503, 504}
MAX_ATTEMPTS = 5
BASE, CAP = 1.0, 60.0

def fetch(url):
    for attempt in range(MAX_ATTEMPTS):
        try:
            r = requests.get(url, proxies=PROXIES, timeout=20)
        except requests.RequestException:
            pass                                  # transient: fall through to backoff
        else:
            if r.status_code == 200 and is_valid(r.text):
                return r.text                     # validate the body, not just the code
            if r.status_code not in TRANSIENT:
                return None                       # terminal: do not retry
            after = r.headers.get("Retry-After")
            if after:
                time.sleep(min(float(after), CAP)) # the target told you the answer
                continue

        ceiling = min(CAP, BASE * (2 ** attempt))
        time.sleep(random.uniform(0, ceiling))     # full jitter, not a fixed delay
    return None

Rotar o esperar: la decisión que los reintentos suelen equivocar

Con proxies residenciales hay un segundo eje. Un fallo puede responderse con tiempo, con una dirección distinta, o con ambos, y elegir mal desperdicia uno de los dos.

Espera cuando la señal es sobre el ritmo. Un 429 o un Retry-After es el objetivo diciendo que tu ritmo es demasiado alto, y cambiar a una dirección nueva para poder mantener el mismo ritmo es precisamente el comportamiento que parece evasión y hace que se queme todo un pool en lugar de una sola ruta. Baja el ritmo en su lugar.

Rota cuando la señal es sobre la dirección. Una página de bloqueo, un 403 persistente, o un desafío que sigue apareciendo en una ruta significa que esa dirección concreta ya no es de confianza, y esperar no la restaurará. Retira la ruta y continúa con una nueva, que es el patrón de failover, y ten en cuenta que la reputación de la sustituta es lo que determina si el reintento realmente ayuda. El único caso en el que rotar es un error es el trabajo a mitad de sesión: si un flujo depende de una identidad mantenida, cambiar de dirección lo rompe, así que un fallo dentro de una sticky session significa reiniciar la secuencia en una sesión nueva en lugar de cambiar direcciones bajo la sesión existente.

Los timeouts se sitúan entre ambos y merecen su propio diagnóstico en lugar de un reflejo, ya que los motivos por los que las peticiones agotan el tiempo de espera incluyen la lentitud del objetivo, una ruta poco saludable y tu propia concurrencia siendo demasiado alta.

Presupuestos y disyuntores (circuit breakers)

Las reglas de reintento por petición no bastan por sí solas, porque no tienen visión del sistema. Dos mecanismos te dan esa visión.

Un presupuesto de reintentos limita los reintentos como proporción del tráfico total, por ejemplo permitiendo que los reintentos sean como máximo el diez por ciento de las peticiones a un objetivo dado. En condiciones normales el presupuesto nunca se toca. Cuando algo se rompe de forma generalizada, el presupuesto se agota de inmediato y los reintentos adicionales simplemente no ocurren, que es la propiedad que quieres: los reintentos ayudan con fallos aislados y son activamente perjudiciales durante una caída generalizada, y un presupuesto es lo que distingue automáticamente entre ambos casos.

Un disyuntor va más allá. Registra la tasa de fallos por objetivo, y cuando cruza un umbral, deja de enviar tráfico a ese objetivo por completo durante un periodo de enfriamiento en lugar de seguir sondeándolo con un goteo de peticiones condenadas. Después del enfriamiento, deja pasar un pequeño número de peticiones, y si tienen éxito, reanuda. Esto protege al objetivo de tu acumulación y protege a tus direcciones de acumular fallos contra un sitio que ahora mismo no responde a nadie. Ambos mecanismos son por objetivo en lugar de globales, porque un sitio roto nunca debería detener un trabajo que está recopilando datos de otros cincuenta.

Los reintentos cuestan dinero y se esconden en tus datos

Dos consecuencias que merece la pena decir con claridad.

Cada reintento es una petición que pagas. En proxies residenciales con precio por ancho de banda, una tormenta de reintentos es una partida de gasto, y un trabajo que reintenta silenciosamente cinco veces contra un sitio que está caído toda la tarde puede mover una cantidad sorprendente de datos para nada, lo cual es uno de los contribuyentes menos obvios al panorama de costes en cutting proxy bandwidth costs.

Y la tasa de reintentos es un indicador adelantado que debería estar en tu panel de control. Una tasa de reintentos creciente en un objetivo es la advertencia más temprana de que sus defensas cambiaron o tu ritmo se desvió, y aparece mucho antes de que la tasa de éxito se desplome, así que monitoring the pipeline debería registrar intentos y resultados, no solo los resultados finales. Un scraper que reintenta silenciosamente hasta lograr una tasa de éxito de apariencia normal está ocultando el problema, no resolviéndolo.

Por último, cuando una petición agota sus intentos, no la descartes. Envíala a una cola de mensajes fallidos (dead-letter queue) y reinténtala mucho más tarde, en la siguiente ejecución o tras un largo enfriamiento, en lugar de en la ráfaga actual. La mayoría de los elementos que fallan durante un incidente tienen éxito por sí solos una hora después, y una cola convierte un fallo duro en uno diferido.

Conclusión

La lógica de reintentos existe para absorber fallos transitorios, no para insistir. Clasifica el fallo antes de actuar, reintenta solo lo que sea genuinamente transitorio, y valida los cuerpos de respuesta para que un bloqueo blando se trate como el fallo que es. Aplica backoff exponencial y siempre con jitter, respeta Retry-After, y limita tanto el retraso como los intentos. Distingue las señales de ritmo, que piden esperar, de las señales de dirección, que piden rotar, y nunca respondas a una limitación cambiando de direcciones para mantener el mismo ritmo. Añade un presupuesto de reintentos y un disyuntor por objetivo para que una caída generalizada no pueda convertirse en una inundación autoinfligida. Haz esto y la mayoría de los bloqueos que los equipos atribuyen a una escalada antibot sencillamente dejan de ocurrir, porque nunca fueron una escalada en primer lugar.

La otra mitad es tener adónde recurrir en caso de fallo, que es lo que ofrecen los residential proxies: un gran pool de IPs reales de tipo residencial, de modo que una ruta retirada se sustituye por una limpia en lugar de por la misma dirección intentándolo de nuevo. El precio por GB es también la razón por la que los reintentos disciplinados se amortizan solos, ya que solo pagas por las peticiones que realmente haces, y una tormenta que nunca envías es ancho de banda que nunca compras.

¿Listo para empezar?

Prueba los proxies residenciales de Shifter, más de 205M IPs, más de 195 países, desde 0,75 $/GB.

Comenzar