Volver al blog

Balanceo de carga a través de proxy: cómo escalar el scraping y las solicitudes API a más de 10000 RPS

Guía completa sobre la distribución de carga a través de grupos de proxies para el scraping de marketplaces, trabajo con API y automatización: arquitectura, algoritmos de balanceo y ejemplos prácticos de configuración.

📅7 de febrero de 2026
```html

Cuando scrapeas miles de páginas de marketplaces, envías solicitudes API masivas o automatizas el trabajo con cientos de cuentas, la correcta distribución de carga a través de proxies se vuelve críticamente importante. Sin un balanceo adecuado, te enfrentarás a bloqueos, timeouts y caídas en el rendimiento. En esta guía, analizaremos la arquitectura de distribución de carga, los algoritmos de balanceo y las estrategias prácticas para sistemas de alta carga.

Este material está dirigido a desarrolladores y especialistas técnicos que trabajan con scraping de datos, automatización de solicitudes API o gestionan grandes grupos de proxies para tareas comerciales.

¿Por qué es necesario el balanceo de carga a través de proxies?

El balanceo de carga a través de proxies resuelve varios problemas críticos de sistemas de alta carga. El primero y más importante es la protección contra bloqueos. Cuando envías miles de solicitudes a un solo recurso (marketplace, API de redes sociales, motor de búsqueda), el servidor objetivo ve una actividad anormalmente alta desde una sola dirección IP y la bloquea. Distribuir las solicitudes entre decenas o cientos de proxies hace que tu actividad se asemeje a la de usuarios normales.

El segundo problema es el rendimiento. Un proxy tiene un ancho de banda limitado (generalmente de 100 a 1000 Mbit/s) y puede manejar un número limitado de conexiones simultáneas. Al hacer scraping de 10,000 páginas por minuto o enviar solicitudes API masivas, un solo proxy se convertirá en un cuello de botella del sistema. El balanceo permite escalar horizontalmente el ancho de banda al agregar nuevos proxies al grupo.

La tercera tarea es la confiabilidad. Si uno de los proxies falla (fallo técnico, bloqueo, expiración del alquiler), el sistema redirige automáticamente el tráfico a proxies operativos. Sin un mecanismo de balanceo y health checks, la falla de un proxy puede detener todo el sistema.

Ejemplo real: Al hacer scraping de Wildberries con el objetivo de monitorear los precios de los competidores, necesitas procesar 50,000 productos cada hora. Esto equivale a aproximadamente 14 solicitudes por segundo. Un proxy puede soportar esta carga, pero Wildberries bloqueará la IP después de 100-200 solicitudes desde una sola dirección. Al distribuir las solicitudes entre 20 proxies residenciales, reduces la carga en cada IP a 0.7 solicitudes por segundo, lo que se asemeja a la actividad de un usuario normal.

La cuarta razón es la distribución geográfica. Muchos servicios muestran contenido o precios diferentes según la región del usuario. El balanceo entre proxies de diferentes países y ciudades permite recopilar datos de todas las regiones objetivo simultáneamente. Por ejemplo, para monitorear precios en Ozon, necesitas proxies de Moscú, San Petersburgo, Ekaterimburgo y otras grandes ciudades.

Arquitectura del sistema de distribución de carga

La arquitectura clásica de un sistema de balanceo de carga a través de proxies consta de varios componentes. En el nivel superior se encuentra el balanceador (load balancer) — un módulo de software que recibe tareas entrantes (solicitudes de scraping, llamadas a API) y las distribuye entre los proxies disponibles. El balanceador puede funcionar como un servicio independiente o estar integrado en la aplicación.

El segundo componente es el gestor de grupos de proxies (proxy pool manager). Este almacena una lista de todos los proxies disponibles con sus características: dirección IP, puerto, protocolo (HTTP/SOCKS5), ubicación geográfica, tipo (residencial, móvil, centro de datos), estado actual (activo, bloqueado, en verificación). El gestor de grupos es responsable de agregar nuevos proxies, eliminar los que no funcionan y verificar periódicamente la disponibilidad.

El tercer elemento es el sistema de monitoreo y health checks. Este verifica constantemente la operatividad de cada proxy: envía solicitudes de prueba, mide el tiempo de respuesta y verifica la conexión exitosa. Si un proxy no responde o devuelve errores, el sistema lo marca como no disponible y lo excluye temporalmente de la rotación.

Componente Función Tecnologías
Load Balancer Distribución de solicitudes entre proxies HAProxy, Nginx, bibliotecas de Python/Node.js
Proxy Pool Manager Gestión de la lista de proxies Redis, PostgreSQL, almacenamiento en memoria
Health Check System Verificación de disponibilidad de proxies Tareas programadas, Celery, cron
Rate Limiter Control de frecuencia de solicitudes Algoritmos de token bucket, leaky bucket
Monitoring & Metrics Recopilación de métricas de rendimiento Prometheus, Grafana, ELK Stack

El cuarto componente es el limitador de tasa (rate limiter). Este se asegura de que cada proxy no supere la frecuencia permitida de solicitudes al recurso objetivo. Por ejemplo, si estás haciendo scraping de Instagram y sabes que la plataforma bloquea la IP después de 60 solicitudes por minuto, el limitador de tasa no permitirá enviar más de 50 solicitudes por minuto a través de un solo proxy, dejando un margen de seguridad.

El quinto elemento es el sistema de métricas y análisis. Este recopila datos sobre el rendimiento de cada proxy: número de solicitudes exitosas, número de errores, tiempo de respuesta promedio, porcentaje de bloqueos. Estos datos se utilizan para optimizar los algoritmos de balanceo y detectar proxies problemáticos.

Algoritmos de balanceo: Round Robin, Least Connections, Weighted

El algoritmo Round Robin es el método de balanceo más simple y común. Este recorre secuencialmente los proxies de la lista: la primera solicitud pasa a través del proxy №1, la segunda a través del proxy №2, la tercera a través del proxy №3, y así sucesivamente. Cuando la lista se termina, el balanceador regresa al inicio. Este algoritmo distribuye la carga uniformemente si todos los proxies tienen el mismo rendimiento.

class RoundRobinBalancer:
    def __init__(self, proxies):
        self.proxies = proxies
        self.current_index = 0
    
    def get_next_proxy(self):
        proxy = self.proxies[self.current_index]
        self.current_index = (self.current_index + 1) % len(self.proxies)
        return proxy

# Uso
proxies = [
    "http://proxy1.example.com:8080",
    "http://proxy2.example.com:8080",
    "http://proxy3.example.com:8080"
]

balancer = RoundRobinBalancer(proxies)
for i in range(10):
    proxy = balancer.get_next_proxy()
    print(f"Request {i+1} → {proxy}")

El algoritmo Least Connections (menos conexiones) elige el proxy con el menor número de conexiones activas en ese momento. Esto es útil cuando las solicitudes tienen diferentes duraciones de ejecución. Por ejemplo, al hacer scraping, una solicitud puede tardar 100 ms, mientras que otra puede tardar 5 segundos (si la página se carga lentamente). Least Connections dirige automáticamente nuevas solicitudes a proxies menos cargados.

class LeastConnectionsBalancer:
    def __init__(self, proxies):
        self.proxies = {proxy: 0 for proxy in proxies}
    
    def get_next_proxy(self):
        # Elegir el proxy con el menor número de conexiones
        proxy = min(self.proxies.items(), key=lambda x: x[1])[0]
        self.proxies[proxy] += 1
        return proxy
    
    def release_proxy(self, proxy):
        # Disminuir el contador al finalizar la solicitud
        self.proxies[proxy] -= 1

# Uso con un gestor de contexto
balancer = LeastConnectionsBalancer(proxies)

def make_request(url):
    proxy = balancer.get_next_proxy()
    try:
        # Realizar la solicitud a través del proxy elegido
        response = requests.get(url, proxies={"http": proxy})
        return response
    finally:
        balancer.release_proxy(proxy)

Weighted Round Robin (round robin ponderado) amplía el clásico Round Robin, asignando un peso a cada proxy en función de su rendimiento o calidad. Los proxies con mayor peso reciben más solicitudes. Esto es útil cuando tienes proxies de diferentes calidades: por ejemplo, proxies residenciales premium con alta velocidad y proxies de centro de datos baratos con limitaciones.

class WeightedRoundRobinBalancer:
    def __init__(self, weighted_proxies):
        # weighted_proxies = [(proxy, weight), ...]
        self.proxies = []
        for proxy, weight in weighted_proxies:
            # Agregar el proxy a la lista weight veces
            self.proxies.extend([proxy] * weight)
        self.current_index = 0
    
    def get_next_proxy(self):
        proxy = self.proxies[self.current_index]
        self.current_index = (self.current_index + 1) % len(self.proxies)
        return proxy

# Uso con pesos
weighted_proxies = [
    ("http://premium-proxy1.com:8080", 5),  # alta calidad
    ("http://premium-proxy2.com:8080", 5),
    ("http://cheap-proxy1.com:8080", 2),    # baja calidad
    ("http://cheap-proxy2.com:8080", 1)     # muy baja calidad
]

balancer = WeightedRoundRobinBalancer(weighted_proxies)

El algoritmo Random (selección aleatoria) elige un proxy al azar de la lista de disponibles. Es más simple de implementar que Round Robin y funciona bien con un gran número de proxies (más de 100). La distribución aleatoria se equilibra automáticamente con un volumen suficiente de solicitudes. La desventaja es que puede haber cortos períodos de carga desigual.

IP Hash es un algoritmo que elige un proxy basado en el hash de la URL objetivo u otro parámetro. Esto garantiza que las solicitudes al mismo recurso siempre pasen a través del mismo proxy. Es útil para sitios que utilizan sesiones o requieren secuencias de solicitudes desde una misma IP (por ejemplo, autorización + obtención de datos).

Gestión de grupos de proxies: rotación y health checks

La gestión efectiva de un grupo de proxies requiere monitoreo constante del estado de cada proxy y rotación automática. Un health check es una verificación periódica de la disponibilidad de proxies mediante el envío de una solicitud de prueba. Generalmente se utiliza una simple solicitud GET a un servicio confiable (por ejemplo, httpbin.org o un endpoint propio) que devuelve información sobre la dirección IP.

import requests
import time
from datetime import datetime

class ProxyHealthChecker:
    def __init__(self, test_url="http://httpbin.org/ip", timeout=10):
        self.test_url = test_url
        self.timeout = timeout
    
    def check_proxy(self, proxy_url):
        """Verifica la operatividad del proxy"""
        try:
            start_time = time.time()
            response = requests.get(
                self.test_url,
                proxies={"http": proxy_url, "https": proxy_url},
                timeout=self.timeout
            )
            response_time = time.time() - start_time
            
            if response.status_code == 200:
                return {
                    "status": "healthy",
                    "response_time": response_time,
                    "timestamp": datetime.now(),
                    "ip": response.json().get("origin")
                }
            else:
                return {
                    "status": "unhealthy",
                    "error": f"HTTP {response.status_code}",
                    "timestamp": datetime.now()
                }
        except requests.exceptions.Timeout:
            return {
                "status": "unhealthy",
                "error": "timeout",
                "timestamp": datetime.now()
            }
        except Exception as e:
            return {
                "status": "unhealthy",
                "error": str(e),
                "timestamp": datetime.now()
            }

# Uso
checker = ProxyHealthChecker()
proxies = ["http://proxy1.com:8080", "http://proxy2.com:8080"]

for proxy in proxies:
    result = checker.check_proxy(proxy)
    print(f"{proxy}: {result['status']} ({result.get('response_time', 'N/A')}s)")

El sistema de gestión de grupos debe marcar automáticamente los proxies no operativos como no disponibles y repetir la verificación periódicamente. Una estrategia común es el patrón de cortocircuito: después de tres verificaciones fallidas consecutivas, el proxy se excluye del grupo durante 5-10 minutos, y luego se verifica nuevamente. Si la verificación es exitosa, el proxy regresa al grupo activo.

class ProxyPoolManager:
    def __init__(self, health_checker, max_failures=3, cooldown_seconds=300):
        self.health_checker = health_checker
        self.max_failures = max_failures
        self.cooldown_seconds = cooldown_seconds
        
        self.proxies = {}  # {proxy_url: ProxyInfo}
    
    def add_proxy(self, proxy_url, metadata=None):
        """Agrega un proxy al grupo"""
        self.proxies[proxy_url] = {
            "url": proxy_url,
            "status": "active",
            "failures": 0,
            "last_check": None,
            "cooldown_until": None,
            "metadata": metadata or {}
        }
    
    def get_active_proxies(self):
        """Devuelve una lista de proxies activos"""
        now = datetime.now()
        active = []
        
        for proxy_url, info in self.proxies.items():
            # Verifica si el proxy está en cooldown
            if info["cooldown_until"] and now < info["cooldown_until"]:
                continue
            
            if info["status"] == "active":
                active.append(proxy_url)
        
        return active
    
    def mark_failure(self, proxy_url):
        """Marca un intento fallido de uso del proxy"""
        if proxy_url not in self.proxies:
            return
        
        info = self.proxies[proxy_url]
        info["failures"] += 1
        
        if info["failures"] >= self.max_failures:
            # Poner en cooldown
            info["status"] = "cooldown"
            info["cooldown_until"] = datetime.now() + timedelta(seconds=self.cooldown_seconds)
            print(f"Proxy {proxy_url} moved to cooldown until {info['cooldown_until']}")
    
    def mark_success(self, proxy_url):
        """Marca un uso exitoso del proxy"""
        if proxy_url not in self.proxies:
            return
        
        info = self.proxies[proxy_url]
        info["failures"] = 0
        info["status"] = "active"
        info["cooldown_until"] = None

La rotación de proxies es una estrategia de cambio automático de proxies a intervalos determinados o después de un número específico de solicitudes. Existen varios enfoques: rotación por tiempo (cambio cada 5-10 minutos), rotación por número de solicitudes (cambio después de 100-500 solicitudes), rotación por sesiones (un proxy por sesión de scraping). La elección de la estrategia depende de los requisitos del sitio objetivo.

Consejo: Para hacer scraping de marketplaces (Wildberries, Ozon) es óptima la rotación por número de solicitudes: 50-100 solicitudes por proxy, luego cambio. Para trabajar con APIs de redes sociales (Instagram, Facebook) es mejor usar rotación por sesiones: un proxy para todo el ciclo de autorización → acciones → salida.

Limitación de tasa y control de frecuencia de solicitudes

La limitación de tasa es un componente críticamente importante del sistema de balanceo de carga. Previene el exceso de la frecuencia permitida de solicitudes al recurso objetivo, lo que podría llevar al bloqueo de proxies. Existen dos algoritmos principales: Token Bucket (cubo de tokens) y Leaky Bucket (cubo con fugas).

Token Bucket funciona de la siguiente manera: cada proxy tiene un "cubo" virtual con tokens. Cada token da derecho a una solicitud. El cubo se llena gradualmente con tokens a una velocidad determinada (por ejemplo, 10 tokens por segundo). La capacidad máxima del cubo es limitada (por ejemplo, 50 tokens). Cuando llega una solicitud, el sistema verifica la disponibilidad de tokens: si hay, toma uno y realiza la solicitud; si no, espera a que aparezca un nuevo token.

import time
from threading import Lock

class TokenBucketRateLimiter:
    def __init__(self, rate, capacity):
        """
        rate: cantidad de tokens por segundo
        capacity: cantidad máxima de tokens en el cubo
        """
        self.rate = rate
        self.capacity = capacity
        self.tokens = capacity
        self.last_update = time.time()
        self.lock = Lock()
    
    def _add_tokens(self):
        """Agrega tokens según el tiempo transcurrido"""
        now = time.time()
        elapsed = now - self.last_update
        new_tokens = elapsed * self.rate
        
        self.tokens = min(self.capacity, self.tokens + new_tokens)
        self.last_update = now
    
    def acquire(self, tokens=1):
        """Intenta obtener la cantidad especificada de tokens"""
        with self.lock:
            self._add_tokens()
            
            if self.tokens >= tokens:
                self.tokens -= tokens
                return True
            return False
    
    def wait_and_acquire(self, tokens=1):
        """Espera a que aparezcan tokens y los obtiene"""
        while not self.acquire(tokens):
            # Calcula el tiempo de espera
            wait_time = (tokens - self.tokens) / self.rate
            time.sleep(wait_time)

# Uso para cada proxy
proxy_limiters = {
    "http://proxy1.com:8080": TokenBucketRateLimiter(rate=10, capacity=50),
    "http://proxy2.com:8080": TokenBucketRateLimiter(rate=10, capacity=50)
}

def make_request_with_limit(url, proxy_url):
    limiter = proxy_limiters[proxy_url]
    limiter.wait_and_acquire()  # Espera la disponibilidad del token
    
    response = requests.get(url, proxies={"http": proxy_url})
    return response

Leaky Bucket (cubo con fugas) funciona de manera diferente: las solicitudes se colocan en una cola (cubos) que "gotea" a una velocidad constante. Si el cubo se desborda, nuevas solicitudes son rechazadas o se colocan en espera. Este algoritmo asegura una distribución más uniforme de la carga a lo largo del tiempo, pero puede causar retrasos durante picos de actividad.

Diferentes plataformas objetivo requieren diferentes límites. La API de Instagram permite aproximadamente 200 solicitudes por hora por IP (alrededor de 3.3 solicitudes por minuto). Wildberries bloquea después de 100-200 solicitudes por minuto desde una IP. Google Search permite 10-20 solicitudes por minuto. Tu sistema de limitación de tasa debe tener en cuenta estas restricciones y agregar un margen de seguridad (generalmente del 20-30%).

Plataforma Límite de solicitudes Configuración recomendada
Instagram ~200 solicitudes/hora 2-3 solicitudes/minuto (con margen)
Wildberries 100-200 solicitudes/minuto 60-80 solicitudes/minuto
Google Search 10-20 solicitudes/minuto 8-12 solicitudes/minuto
Ozon 50-100 solicitudes/minuto 30-50 solicitudes/minuto
Facebook API 200 solicitudes/hora 2-3 solicitudes/minuto

Monitoreo del rendimiento y escalado automático

Un sistema efectivo de balanceo de carga requiere monitoreo constante de métricas clave. El primer grupo de métricas son las de rendimiento de proxies: tiempo de respuesta promedio (response time), porcentaje de solicitudes exitosas (success rate), número de timeouts, número de errores 4xx y 5xx. Estos datos permiten identificar proxies problemáticos y optimizar los algoritmos de balanceo.

class ProxyMetrics:
    def __init__(self):
        self.metrics = {}  # {proxy_url: metrics_dict}
    
    def record_request(self, proxy_url, response_time, status_code, success):
        """Registra las métricas de la solicitud"""
        if proxy_url not in self.metrics:
            self.metrics[proxy_url] = {
                "total_requests": 0,
                "successful_requests": 0,
                "failed_requests": 0,
                "total_response_time": 0,
                "timeouts": 0,
                "errors_4xx": 0,
                "errors_5xx": 0
            }
        
        m = self.metrics[proxy_url]
        m["total_requests"] += 1
        
        if success:
            m["successful_requests"] += 1
            m["total_response_time"] += response_time
        else:
            m["failed_requests"] += 1
            
            if status_code == 0:  # timeout
                m["timeouts"] += 1
            elif 400 <= status_code < 500:
                m["errors_4xx"] += 1
            elif 500 <= status_code < 600:
                m["errors_5xx"] += 1
    
    def get_success_rate(self, proxy_url):
        """Devuelve el porcentaje de solicitudes exitosas"""
        m = self.metrics.get(proxy_url, {})
        total = m.get("total_requests", 0)
        if total == 0:
            return 0
        return (m.get("successful_requests", 0) / total) * 100
    
    def get_avg_response_time(self, proxy_url):
        """Devuelve el tiempo de respuesta promedio"""
        m = self.metrics.get(proxy_url, {})
        successful = m.get("successful_requests", 0)
        if successful == 0:
            return 0
        return m.get("total_response_time", 0) / successful
    
    def get_report(self, proxy_url):
        """Devuelve un informe completo del proxy"""
        m = self.metrics.get(proxy_url, {})
        return {
            "proxy": proxy_url,
            "total_requests": m.get("total_requests", 0),
            "success_rate": self.get_success_rate(proxy_url),
            "avg_response_time": self.get_avg_response_time(proxy_url),
            "timeouts": m.get("timeouts", 0),
            "errors_4xx": m.get("errors_4xx", 0),
            "errors_5xx": m.get("errors_5xx", 0)
        }

El segundo grupo de métricas es el rendimiento general del sistema: número total de solicitudes por segundo (RPS — requests per second), latencia promedio (latency), tamaño de la cola de solicitudes, número de proxies activos, porcentaje de uso del grupo de proxies. Estas métricas muestran si el sistema está manejando la carga o si se requiere escalado.

El escalado automático (auto-scaling) permite agregar o eliminar proxies dinámicamente según la carga. Una estrategia simple: si la carga promedio del grupo supera el 80% durante 5 minutos, el sistema agrega automáticamente nuevos proxies. Si la carga cae por debajo del 30% durante 15 minutos, el sistema elimina proxies redundantes para ahorrar recursos.

Ejemplo de configuración de monitoreo: Para hacer scraping de 100,000 productos de Wildberries por hora (aproximadamente 28 solicitudes por segundo), necesitarás al menos 30-40 proxies con un límite de 60 solicitudes por minuto por proxy. Configura alertas: si el porcentaje de éxito cae por debajo del 85% o el tiempo de respuesta promedio supera los 3 segundos, el sistema debe agregar automáticamente 10-15 proxies de reserva del grupo.

Para visualizar métricas, utiliza Grafana con Prometheus o ELK Stack. Crea dashboards con gráficos: RPS a lo largo del tiempo, distribución del tiempo de respuesta, top-10 proxies más rápidos/lentos, mapa de errores por tipos. Esto permitirá identificar rápidamente problemas y optimizar el sistema.

Implementación práctica en Python y Node.js

Consideremos una implementación completa de un sistema de balanceo de carga en Python utilizando bibliotecas populares. Este ejemplo integra todos los componentes descritos: balanceador, gestor de grupos, health checks, limitación de tasa y monitoreo.

import requests
import time
import random
from datetime import datetime, timedelta
from threading import Lock, Thread
from collections import defaultdict

class ProxyLoadBalancer:
    def __init__(self, proxies, algorithm="round_robin", rate_limit=10):
        """
        proxies: lista de proxies [{"url": "...", "weight": 1}, ...]
        algorithm: round_robin, least_connections, weighted, random
        rate_limit: número máximo de solicitudes por segundo por proxy
        """
        self.proxies = proxies
        self.algorithm = algorithm
        self.rate_limit = rate_limit
        
        # Estado del balanceador
        self.current_index = 0
        self.connections = defaultdict(int)
        self.rate_limiters = {}
        self.metrics = defaultdict(lambda: {
            "total": 0, "success": 0, "failed": 0,
            "response_times": [], "last_check": None
        })
        
        # Inicialización de limitadores de tasa
        for proxy in proxies:
            proxy_url = proxy["url"]
            self.rate_limiters[proxy_url] = TokenBucketRateLimiter(
                rate=rate_limit,
                capacity=rate_limit * 5
            )
        
        self.lock = Lock()
        
        # Iniciar el health check en segundo plano
        self.health_check_thread = Thread(target=self._health_check_loop, daemon=True)
        self.health_check_thread.start()
    
    def get_next_proxy(self):
        """Selecciona el siguiente proxy según el algoritmo"""
        with self.lock:
            if self.algorithm == "round_robin":
                return self._round_robin()
            elif self.algorithm == "least_connections":
                return self._least_connections()
            elif self.algorithm == "weighted":
                return self._weighted_random()
            elif self.algorithm == "random":
                return random.choice(self.proxies)["url"]
    
    def _round_robin(self):
        proxy = self.proxies[self.current_index]["url"]
        self.current_index = (self.current_index + 1) % len(self.proxies)
        return proxy
    
    def _least_connections(self):
        min_conn = min(self.connections.values()) if self.connections else 0
        candidates = [p["url"] for p in self.proxies if self.connections[p["url"]] == min_conn]
        return random.choice(candidates)
    
    def _weighted_random(self):
        weights = [p.get("weight", 1) for p in self.proxies]
        return random.choices(self.proxies, weights=weights)[0]["url"]
    
    def make_request(self, url, method="GET", **kwargs):
        """Realiza una solicitud a través del balanceador"""
        proxy_url = self.get_next_proxy()
        
        # Espera la disponibilidad del límite de tasa
        self.rate_limiters[proxy_url].wait_and_acquire()
        
        # Aumenta el contador de conexiones
        with self.lock:
            self.connections[proxy_url] += 1
        
        try:
            start_time = time.time()
            response = requests.request(
                method,
                url,
                proxies={"http": proxy_url, "https": proxy_url},
                timeout=kwargs.get("timeout", 30),
                **kwargs
            )
            response_time = time.time() - start_time
            
            # Registra métricas
            self._record_metrics(proxy_url, response_time, True, response.status_code)
            
            return response
        
        except Exception as e:
            self._record_metrics(proxy_url, 0, False, 0)
            raise
        
        finally:
            # Disminuye el contador de conexiones
            with self.lock:
                self.connections[proxy_url] -= 1
    
    def _record_metrics(self, proxy_url, response_time, success, status_code):
        """Registra las métricas de la solicitud"""
        with self.lock:
            m = self.metrics[proxy_url]
            m["total"] += 1
            if success:
                m["success"] += 1
                m["response_times"].append(response_time)
                # Almacena solo los últimos 1000 valores
                if len(m["response_times"]) > 1000:
                    m["response_times"].pop(0)
            else:
                m["failed"] += 1
    
    def _health_check_loop(self):
        """Verificación de la operatividad de los proxies en segundo plano"""
        while True:
            for proxy in self.proxies:
                proxy_url = proxy["url"]
                try:
                    response = requests.get(
                        "http://httpbin.org/ip",
                        proxies={"http": proxy_url},
                        timeout=10
                    )
                    with self.lock:
                        self.metrics[proxy_url]["last_check"] = datetime.now()
                        proxy["status"] = "healthy" if response.status_code == 200 else "unhealthy"
                except:
                    with self.lock:
                        proxy["status"] = "unhealthy"
            
            time.sleep(60)  # Verificación cada minuto
    
    def get_stats(self):
        """Devuelve estadísticas de todos los proxies"""
        stats = []
        with self.lock:
            for proxy in self.proxies:
                proxy_url = proxy["url"]
                m = self.metrics[proxy_url]
                
                avg_response_time = (
                    sum(m["response_times"]) / len(m["response_times"])
                    if m["response_times"] else 0
                )
                
                success_rate = (
                    (m["success"] / m["total"] * 100)
                    if m["total"] > 0 else 0
                )
                
                stats.append({
                    "proxy": proxy_url,
                    "status": proxy.get("status", "unknown"),
                    "total_requests": m["total"],
                    "success_rate": round(success_rate, 2),
                    "avg_response_time": round(avg_response_time, 3),
                    "active_connections": self.connections[proxy_url]
                })
        
        return stats

Uso de este balanceador para hacer scraping de un marketplace:

# Configuración del balanceador
proxies = [
    {"url": "http://proxy1.example.com:8080", "weight": 5},
    {"url": "http://proxy2.example.com:8080", "weight": 5},
    {"url": "http://proxy3.example.com:8080", "weight": 3},
    {"url": "http://proxy4.example.com:8080", "weight": 2}
]

balancer = ProxyLoadBalancer(
    proxies=proxies,
    algorithm="weighted",
    rate_limit=60  # 60 solicitudes por minuto por proxy
)

# Hacer scraping de la lista de productos
product_urls = [f"https://www.wildberries.ru/catalog/{i}/detail.aspx" for i in range(1000)]

results = []
for url in product_urls:
    try:
        response = balancer.make_request(url)
        # Procesar la respuesta
        results.append({"url": url, "status": "success", "data": response.text})
    except Exception as e:
        results.append({"url": url, "status": "error", "error": str(e)})
    
    # Cada 100 solicitudes, imprimir estadísticas
    if len(results) % 100 == 0:
        stats = balancer.get_stats()
        for stat in stats:
            print(f"{stat['proxy']}: {stat['success_rate']}% success, "
                  f"{stat['avg_response_time']}s avg response")

# Estadísticas finales
print("\n=== Estadísticas Finales ===")
for stat in balancer.get_stats():
    print(f"{stat['proxy']}:")
    print(f"  Total de solicitudes: {stat['total_requests']}")
    print(f"  Tasa de éxito: {stat['success_rate']}%")
    print(f"  Tiempo de respuesta promedio: {stat['avg_response_time']}s")
    print(f"  Estado: {stat['status']}")

Para Node.js, puedes usar una arquitectura similar con las bibliotecas axios para solicitudes HTTP y node-rate-limiter para controlar la frecuencia:

const axios = require('axios');
const { RateLimiter } = require('limiter');

class ProxyLoadBalancer {
  constructor(proxies, algorithm = 'round_robin', rateLimit = 10) {
    this.proxies = proxies;
    this.algorithm = algorithm;
    this.currentIndex = 0;
    this.connections = new Map();
    this.limiters = new Map();
    this.metrics = new Map();
    
    // Inicialización de limitadores de tasa
    proxies.forEach(proxy => {
      this.limiters.set(proxy.url, new RateLimiter({ 
        tokensPerInterval: rateLimit, 
        interval: 'second' 
      }));
      this.connections.set(proxy.url, 0);
      this.metrics.set(proxy.url, {
        total: 0,
        success: 0,
        failed: 0,
        responseTimes: []
      });
    });
  }
  
  getNextProxy() {
    if (this.algorithm === 'round_robin') {
      const proxy = this.proxies[this.currentIndex].url;
      this.currentIndex = (this.currentIndex + 1) % this.proxies.length;
      return proxy;
    } else if (this.algorithm === 'least_connections') {
      let minConn = Infinity;
      let selectedProxy = null;
      
      this.connections.forEach((count, proxy) => {
        if (count < minConn) {
          minConn = count;
          selectedProxy = proxy;
        }
      });
      
      return selectedProxy;
    }
  }
  
  async makeRequest(url, options = {}) {
    const proxyUrl = this.getNextProxy();
    const limiter = this.limiters.get(proxyUrl);
    
    // Espera la disponibilidad del límite de tasa
    await limiter.removeTokens(1);
    
    // Aumenta el contador de conexiones
    this.connections.set(proxyUrl, this.connections.get(proxyUrl) + 1);
    
    try {
      const startTime = Date.now();
      const response = await axios({
        url,
        proxy: this.parseProxyUrl(proxyUrl),
        timeout: options.timeout || 30000,
        ...options
      });
      
      const responseTime = (Date.now() - startTime) / 1000;
      this.recordMetrics(proxyUrl, responseTime, true);
      
      return response;
    } catch (error) {
      this.recordMetrics(proxyUrl, 0, false);
      throw error;
    } finally {
      this.connections.set(proxyUrl, this.connections.get(proxyUrl) - 1);
    }
  }
  
  parseProxyUrl(proxyUrl) {
    const url = new URL(proxyUrl);
    return {
      host: url.hostname,
      port: parseInt(url.port)
    };
  }
  
  recordMetrics(proxyUrl, responseTime, success) {
    const m = this.metrics.get(proxyUrl);
    m.total++;
    
    if (success) {
      m.success++;
      m.responseTimes.push(responseTime);
      if (m.responseTimes.length > 1000) {
        m.responseTimes.shift();
      }
    } else {
      m.failed++;
    }
  }
  
  getStats() {
    const stats = [];
    
    this.proxies.forEach(proxy => {
      const m = this.metrics.get(proxy.url);
      const avgResponseTime = m.responseTimes.length > 0
        ? m.responseTimes.reduce((a, b) => a + b, 0) / m.responseTimes.length
        : 0;
      
      const successRate = m.total > 0 ? (m.success / m.total * 100) : 0;
      
      stats.push({
        proxy: proxy.url,
        totalRequests: m.total,
        successRate: successRate.toFixed(2),
        avgResponseTime: avgResponseTime.toFixed(3),
        activeConnections: this.connections.get(proxy.url)
      });
    });
    
    return stats;
  }
}

// Uso
const proxies = [
  { url: 'http://proxy1.example.com:8080', weight: 5 },
  { url: 'http://proxy2.example.com:8080', weight: 5 }
];

const balancer = new ProxyLoadBalancer(proxies, 'round_robin', 60);

async function parseProducts() {
  const urls = Array.from({ length: 1000 }, (_, i) => 
    `https://www.wildberries.ru/catalog/${i}/detail.aspx`
  );
  
  for (const url of urls) {
    try {
      const response = await balancer.makeRequest(url);
      console.log(`Success: ${url}`);
    } catch (error) {
      console.error(`Error: ${url} - ${error.message}`);
    }
  }
  
  console.log('\n=== Estadísticas ===');
  console.log(balancer.getStats());
}

parseProducts();

Optimización para tareas específicas: scraping, API, automatización

Diferentes tareas requieren diferentes estrategias de balanceo de carga. Para hacer scraping de marketplaces (Wildberries, Ozon, Avito), la estrategia óptima es la rotación por número de solicitudes y distribución geográfica. Utiliza proxies residenciales de diferentes ciudades de Rusia, cambia proxies cada 50-100 solicitudes, y agrega retrasos aleatorios entre solicitudes (1-3 segundos) para simular el comportamiento humano.

Para trabajar con APIs de redes sociales (Instagram, Facebook, VK), la estabilidad de la dirección IP dentro de una sesión es crítica. Utiliza el algoritmo IP Hash o sesiones fijas: todas las solicitudes de una cuenta deben pasar a través del mismo proxy. Esto previene cambios sospechosos de geolocalización que pueden llevar al bloqueo de la cuenta. Se recomiendan proxies móviles, ya que las redes sociales confían más en las IP móviles que en las residenciales o de centros de datos.

# Ejemplo de sesiones fijas para Instagram
class StickySessionBalancer:
    def __init__(self, proxies):
        self.proxies = proxies
        self.session_map = {}  # {account_id: proxy_url}
        self.proxy_usage = defaultdict(int)
    
    def get_proxy_for_account(self, account_id):
        """Devuelve un proxy fijo para la cuenta"""
        if account_id in self.session_map:
            return self.session_map[account_id]
        
        # Selecciona el proxy menos cargado
        proxy = min(self.proxies, key=lambda p: self.proxy_usage[p])
        self.session_map[account_id] = proxy
        self.proxy_usage[proxy] += 1
        
        return proxy
    
    def release_account(self, account_id):
        """Libera el proxy al finalizar el trabajo con la cuenta"""
        if account_id in self.session_map:
            proxy = self.session_map[account_id]
            self.proxy_usage[proxy] -= 1
            del self.session_map[account_id]
```