Volver al blog

Cómo Corregir Errores de Timeout al Usar Proxy - Soluciones Efectivas

Los errores de timeout a través de proxy son un problema común en web scraping y automatización. Analizamos las causas y ofrecemos soluciones funcionales con ejemplos de código.

📅15 de diciembre de 2025
```html

Cómo corregir errores de timeout al trabajar a través de proxy

La solicitud se colgó, el script falló con un error TimeoutError, los datos no se obtuvieron. ¿Te resulta familiar? Los errores de timeout a través de proxy son uno de los problemas más comunes al hacer web scraping y automatización. Analicemos las causas y proporcionemos soluciones concretas.

Por qué ocurren errores de timeout

El timeout no es un único problema, sino un síntoma. Antes de tratar, necesitas entender la causa:

Servidor proxy lento. Un servidor sobrecargado o un proxy geográficamente lejano añaden retraso a cada solicitud. Si tu timeout es de 10 segundos y el proxy responde en 12 segundos, obtendrás un error.

Bloqueo en el sitio de destino. El sitio puede mantener deliberadamente las solicitudes sospechosas "colgadas" en lugar de rechazarlas explícitamente. Esta es una táctica contra bots: mantener la conexión abierta indefinidamente.

Problemas con DNS. El proxy debe resolver el dominio. Si el servidor DNS del proxy es lento o no está disponible, la solicitud se cuelga en la etapa de conexión.

Configuración incorrecta de timeouts. Un único timeout general para todo es un error común. El timeout de conexión y el timeout de lectura son cosas diferentes y deben configurarse por separado.

Problemas de red. Pérdida de paquetes, conexión inestable del proxy, problemas de enrutamiento: todo esto conduce a timeouts.

Tipos de timeouts y su configuración

La mayoría de las bibliotecas HTTP admiten varios tipos de timeouts. Comprender la diferencia entre ellos es la clave para una configuración correcta.

Timeout de conexión

Tiempo para establecer una conexión TCP con el proxy y el servidor de destino. Si el proxy no está disponible o el servidor no responde, se activará este timeout. Valor recomendado: 5-10 segundos.

Timeout de lectura

Tiempo de espera de datos después de establecer la conexión. El servidor se conectó pero está en silencio: se activará el timeout de lectura. Para páginas normales: 15-30 segundos. Para APIs pesadas: 60+ segundos.

Timeout total

Tiempo total para toda la solicitud de principio a fin. Protección contra conexiones colgadas. Normalmente: conexión + lectura + margen.

Ejemplo de configuración en Python con la biblioteca requests:

import requests

proxies = {
    "http": "http://user:pass@proxy.example.com:8080",
    "https": "http://user:pass@proxy.example.com:8080"
}

# Tupla: (timeout_conexión, timeout_lectura)
timeout = (10, 30)

try:
    response = requests.get(
        "https://target-site.com/api/data",
        proxies=proxies,
        timeout=timeout
    )
except requests.exceptions.ConnectTimeout:
    print("No se pudo conectar al proxy o servidor")
except requests.exceptions.ReadTimeout:
    print("El servidor no envió datos a tiempo")

Para aiohttp (Python asincrónico):

import aiohttp
import asyncio

async def fetch_with_timeout():
    timeout = aiohttp.ClientTimeout(
        total=60,      # Timeout total
        connect=10,    # Para conexión
        sock_read=30   # Para lectura de datos
    )
    
    async with aiohttp.ClientSession(timeout=timeout) as session:
        async with session.get(
            "https://target-site.com/api/data",
            proxy="http://user:pass@proxy.example.com:8080"
        ) as response:
            return await response.text()

Lógica de reintentos: el enfoque correcto

El timeout no siempre es un error fatal. A menudo, una solicitud repetida tiene éxito. Pero los reintentos deben hacerse inteligentemente.

Retardo exponencial

No bombardees el servidor con solicitudes repetidas sin pausa. Utiliza backoff exponencial: cada intento siguiente tiene un retraso cada vez mayor.

import requests
import time
import random

def fetch_with_retry(url, proxies, max_retries=3):
    """Solicitud con reintentos y retardo exponencial"""
    
    for attempt in range(max_retries):
        try:
            response = requests.get(
                url,
                proxies=proxies,
                timeout=(10, 30)
            )
            response.raise_for_status()
            return response
            
        except (requests.exceptions.Timeout, 
                requests.exceptions.ConnectionError) as e:
            
            if attempt == max_retries - 1:
                raise  # Último intento: lanzar el error
            
            # Retardo exponencial: 1s, 2s, 4s...
            # + jitter aleatorio para no crear olas de solicitudes
            delay = (2 ** attempt) + random.uniform(0, 1)
            print(f"Intento {attempt + 1} falló: {e}")
            print(f"Reintentando en {delay:.1f} segundos...")
            time.sleep(delay)

Biblioteca tenacity

Para código de producción es más conveniente usar soluciones listas:

from tenacity import retry, stop_after_attempt, wait_exponential
from tenacity import retry_if_exception_type
import requests

@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=1, max=10),
    retry=retry_if_exception_type((
        requests.exceptions.Timeout,
        requests.exceptions.ConnectionError
    ))
)
def fetch_data(url, proxies):
    response = requests.get(url, proxies=proxies, timeout=(10, 30))
    response.raise_for_status()
    return response.json()

Rotación de proxy en caso de timeouts

Si un proxy constantemente genera timeouts, el problema está en él. La solución lógica es cambiar a otro.

import requests
from collections import deque
from dataclasses import dataclass, field
from typing import Optional
import time

@dataclass
class ProxyManager:
    """Gestor de proxy con seguimiento de intentos fallidos"""
    
    proxies: list
    max_failures: int = 3
    cooldown_seconds: int = 300
    _failures: dict = field(default_factory=dict)
    _cooldown_until: dict = field(default_factory=dict)
    
    def get_proxy(self) -> Optional[str]:
        """Obtener un proxy funcional"""
        current_time = time.time()
        
        for proxy in self.proxies:
            # Saltar proxies en cooldown
            if self._cooldown_until.get(proxy, 0) > current_time:
                continue
            return proxy
        
        return None  # Todos los proxies están en cooldown
    
    def report_failure(self, proxy: str):
        """Reportar una solicitud fallida"""
        self._failures[proxy] = self._failures.get(proxy, 0) + 1
        
        if self._failures[proxy] >= self.max_failures:
            # Enviar proxy a cooldown
            self._cooldown_until[proxy] = time.time() + self.cooldown_seconds
            self._failures[proxy] = 0
            print(f"Proxy {proxy} enviado a cooldown")
    
    def report_success(self, proxy: str):
        """Resetear contador de errores en caso de éxito"""
        self._failures[proxy] = 0


def fetch_with_rotation(url, proxy_manager, max_attempts=5):
    """Solicitud con cambio automático de proxy en caso de errores"""
    
    for attempt in range(max_attempts):
        proxy = proxy_manager.get_proxy()
        
        if not proxy:
            raise Exception("No hay proxies disponibles")
        
        proxies = {"http": proxy, "https": proxy}
        
        try:
            response = requests.get(url, proxies=proxies, timeout=(10, 30))
            response.raise_for_status()
            proxy_manager.report_success(proxy)
            return response
            
        except (requests.exceptions.Timeout, 
                requests.exceptions.ConnectionError):
            proxy_manager.report_failure(proxy)
            print(f"Timeout a través de {proxy}, intentando otro...")
            continue
    
    raise Exception(f"No se pudieron obtener datos después de {max_attempts} intentos")

Al usar proxies residenciales con rotación automática, esta lógica se simplifica: el proveedor cambia automáticamente la IP en cada solicitud o según el intervalo especificado.

Solicitudes asincrónicas con control de timeouts

Para web scraping masivo, las solicitudes sincrónicas son ineficientes. El enfoque asincrónico permite procesar cientos de URLs en paralelo, pero requiere un manejo cuidadoso de los timeouts.

import aiohttp
import asyncio
from typing import List, Tuple

async def fetch_one(
    session: aiohttp.ClientSession, 
    url: str,
    semaphore: asyncio.Semaphore
) -> Tuple[str, str | None, str | None]:
    """Descarga de una URL con manejo de timeout"""
    
    async with semaphore:  # Limitar paralelismo
        try:
            async with session.get(url) as response:
                content = await response.text()
                return (url, content, None)
                
        except asyncio.TimeoutError:
            return (url, None, "timeout")
        except aiohttp.ClientError as e:
            return (url, None, str(e))


async def fetch_all(
    urls: List[str],
    proxy: str,
    max_concurrent: int = 10
) -> List[Tuple[str, str | None, str | None]]:
    """Descarga masiva con control de timeouts y paralelismo"""
    
    timeout = aiohttp.ClientTimeout(total=45, connect=10, sock_read=30)
    semaphore = asyncio.Semaphore(max_concurrent)
    
    connector = aiohttp.TCPConnector(
        limit=max_concurrent,
        limit_per_host=5  # No más de 5 conexiones por host
    )
    
    async with aiohttp.ClientSession(
        timeout=timeout,
        connector=connector
    ) as session:
        # Establecer proxy para todas las solicitudes
        tasks = [
            fetch_one(session, url, semaphore) 
            for url in urls
        ]
        results = await asyncio.gather(*tasks)
    
    # Estadísticas
    success = sum(1 for _, content, _ in results if content)
    timeouts = sum(1 for _, _, error in results if error == "timeout")
    print(f"Exitosas: {success}, Timeouts: {timeouts}")
    
    return results


# Uso
async def main():
    urls = [f"https://example.com/page/{i}" for i in range(100)]
    results = await fetch_all(
        urls, 
        proxy="http://user:pass@proxy.example.com:8080",
        max_concurrent=10
    )

asyncio.run(main())

Importante: No establezca un paralelismo demasiado alto. 50-100 solicitudes simultáneas a través de un proxy es demasiado. Es mejor 10-20 con varios proxies.

Diagnóstico: cómo encontrar la causa

Antes de cambiar la configuración, determine la fuente del problema.

Paso 1: Verificar el proxy directamente

# Prueba simple a través de curl con medición de tiempo
curl -x http://user:pass@proxy:8080 \
     -w "Conexión: %{time_connect}s\nTotal: %{time_total}s\n" \
     -o /dev/null -s \
     https://httpbin.org/get

Si time_connect es mayor a 5 segundos, el problema está en el proxy o la red hacia él.

Paso 2: Comparar con solicitud directa

import requests
import time

def measure_request(url, proxies=None):
    start = time.time()
    try:
        r = requests.get(url, proxies=proxies, timeout=30)
        elapsed = time.time() - start
        return f"OK: {elapsed:.2f}s, estado: {r.status_code}"
    except Exception as e:
        elapsed = time.time() - start
        return f"FALLO: {elapsed:.2f}s, error: {type(e).__name__}"

url = "https://target-site.com"
proxy = {"http": "http://proxy:8080", "https": "http://proxy:8080"}

print("Directo:", measure_request(url))
print("A través de proxy:", measure_request(url, proxy))

Paso 3: Probar diferentes tipos de proxy

Los timeouts pueden depender del tipo de proxy:

Tipo de proxy Retraso típico Timeout recomendado
Datacenter 50-200 ms Conexión: 5s, Lectura: 15s
Residencial 200-800 ms Conexión: 10s, Lectura: 30s
Móvil 300-1500 ms Conexión: 15s, Lectura: 45s

Paso 4: Registrar detalles

import logging
import requests
from requests.adapters import HTTPAdapter

# Habilitar registro de depuración
logging.basicConfig(level=logging.DEBUG)
logging.getLogger("urllib3").setLevel(logging.DEBUG)

# Ahora verá todas las etapas de la solicitud:
# - Resolución de DNS
# - Establecimiento de conexión
# - Envío de solicitud
# - Recepción de respuesta

Lista de verificación para resolver errores de timeout

Algoritmo breve de acciones cuando ocurren timeouts:

  1. Determine el tipo de timeout — ¿conexión o lectura? Son problemas diferentes.
  2. Verifique el proxy por separado — ¿funciona en absoluto? ¿Cuál es el retraso?
  3. Aumente los timeouts — posiblemente los valores sean demasiado agresivos para su tipo de proxy.
  4. Agregue reintentos con backoff — los timeouts únicos son normales, lo importante es la resistencia.
  5. Configure la rotación — cambie automáticamente a otro proxy en caso de problemas.
  6. Limite el paralelismo — demasiadas solicitudes simultáneas sobrecargan el proxy.
  7. Verifique el sitio de destino — posiblemente esté bloqueando o limitando sus solicitudes.

Conclusión

Los errores de timeout a través de proxy son un problema solucionable. En la mayoría de los casos, es suficiente configurar correctamente los timeouts según el tipo de proxy, agregar lógica de reintentos e implementar rotación en caso de fallas. Para tareas con altos requisitos de estabilidad, use proxies residenciales con rotación automática: más información en proxycove.com.

```