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:
- Determine el tipo de timeout — ¿conexión o lectura? Son problemas diferentes.
- Verifique el proxy por separado — ¿funciona en absoluto? ¿Cuál es el retraso?
- Aumente los timeouts — posiblemente los valores sean demasiado agresivos para su tipo de proxy.
- Agregue reintentos con backoff — los timeouts únicos son normales, lo importante es la resistencia.
- Configure la rotación — cambie automáticamente a otro proxy en caso de problemas.
- Limite el paralelismo — demasiadas solicitudes simultáneas sobrecargan el proxy.
- 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.