Conhecimento

Balanceamento de Carga em Proxy Residencial: Rotação, Concorrência e Arquitetura de Retry

Com um gateway, o pool se equilibra sozinho. A arquitetura que importa é a sua: unidades de rotação, concorrência por host e retries que não amplificam bloqueios.

Chris Collins

Chris Collins

17 de julho de 2026 · 11 min de leitura

“Balanceamento de carga de proxy” é uma frase que hoje significa algo muito diferente do que significava antes. No modelo antigo, você comprava uma lista de IPs e construía um rotator: escolher um IP, monitorar sua saúde, retirá-lo quando morria, recolocá-lo depois. Times ainda escrevem esse middleware por hábito, e em um gateway moderno isso é, na maior parte, código morto.

Em um gateway de proxy residencial, o próprio pool se balanceia. Um único endpoint, e o provedor atribui uma saída a partir de milhões de IPs por requisição ou por sessão, do lado do servidor. Você não está balanceando a carga de IPs. O que você está arquitetando é a camada acima: como o trabalho é mapeado em identidades, quanta concorrência um alvo vai tolerar, e o que acontece quando uma requisição falha. Acerte essas três coisas e o sistema escala; erre e nenhuma qualidade de proxy vai salvar você.

Este é um mergulho profundo nessas três camadas, para times que rodam pipelines de coleta em produção.

O que o gateway faz, e onde sua arquitetura começa

Vale a pena ser preciso sobre essa divisão, porque ela determina o que você deve e não deve construir:

O gateway cuida de: seleção do IP de saída a partir do pool, correspondência geográfica, saúde de IPs individuais e a mecânica de rotação. Você expressa a intenção através do username (country, city, sid, ttl) e ele resolve isso em uma saída.

Você cuida de: a unidade de identidade (o que recebe seu próprio IP, e por quanto tempo), controle de concorrência (quão forte você pressiona cada alvo), política de retry (o que acontece na falha) e observabilidade.

O erro arquitetural mais comum é construir uma camada de proxy-rotator que duplica o gateway. Se você se pegar mantendo uma tabela de saúde de IPs, pare, esse é o trabalho do provedor, e sua versão tem uma visão do pool pior do que a dele. Seu trabalho começa no nível da política de requisição.

Camada 1: arquitetura de rotação, escolhendo a unidade de identidade

Essa é a decisão de design da qual tudo mais depende. A pergunta não é “com que frequência devo rotacionar?”, mas “o que é uma identidade, e que trabalho pertence a ela?”

Os dois primitivos:

  • Rotação por requisição (omitindo sid): cada requisição recebe um IP de saída novo. Diversidade máxima de IP, zero reaproveitamento de conexão, ideal para grandes conjuntos de buscas independentes onde nenhuma requisição depende da anterior.
  • Sessão fixa (sticky) (sid + ttl): todas as requisições que carregam aquele id de sessão compartilham um IP de saída até o TTL expirar. Necessário quando um fluxo precisa parecer um único usuário, e é também o que torna as conexões em pool reaproveitáveis (veja sticky vs rotativo).

A regra de design: rotacione na fronteira de uma unidade lógica de trabalho, não arbitrariamente. Uma unidade de trabalho é o que precisa ser internamente consistente, as avaliações paginadas de um produto, um fluxo de busca, a sessão de uma conta. Mapeie isso de forma limpa:

um worker = uma unidade de trabalho = um sid = um IP de saída (durante seu TTL)

Derive o id de sessão a partir da unidade de trabalho em vez de aleatoriamente, para que o comportamento seja reproduzível e você possa decidir deliberadamente se um retry mantém ou muda a identidade:

def session_id(job_id: str, attempt: int = 0) -> str:
# Same job → same identity. Bump `attempt` to deliberately get a new IP.
return f"{job_id}-a{attempt}"
def proxy_for(job_id, attempt=0, country="us", ttl=600):
sid = session_id(job_id, attempt)
user = f"{USER}-country-{country}-sid-{sid}-ttl-{ttl}"
url = f"http://{user}:{PASS}@p.shifter.io:443"
return {"http": url, "https": url}

Esse único parâmetro attempt está fazendo um trabalho arquitetural real: ele torna “retry em uma identidade diferente” uma operação de primeira classe e intencional, em vez de um acidente.

Camada 2: arquitetura de concorrência, e por que ela é por host

A restrição na concorrência quase nunca é o seu plano de proxy (conexões concorrentes tipicamente não são o limite). É o que o alvo tolera. O que significa que um único limite global de concorrência tem a forma errada: ele permite que um alvo permissivo sufoque um alvo frágil, e permite que um alvo frágil estrangule todo o seu pipeline.

Limite por host, não globalmente:

import asyncio
from collections import defaultdict
# One semaphore per target host, tuned to what that host tolerates
_limits = {"tough-site.example": 4, "open-site.example": 32}
_sems = defaultdict(lambda: asyncio.Semaphore(8)) # sensible default
for host, n in _limits.items():
_sems[host] = asyncio.Semaphore(n)
async def fetch(client, url, host):
async with _sems[host]: # backpressure applied per host
return await client.get(url, timeout=30)

Duas propriedades que isso te traz. Isolamento: um alvo lento ou hostil não pode consumir todos os workers. Ajustabilidade: você pode aumentar a concorrência em alvos que não se importam e reduzi-la naquele que bloqueia, de forma independente.

E resista à vontade de aumentar os números. Além da tolerância de um alvo, paralelismo extra não compra throughput, compra bloqueios, e bloqueios custam mais em retries do que a concorrência jamais ganhou (reduzindo latência cobre essa troca).

Camada 3: arquitetura de retry, a parte que faz ou quebra tudo

A maioria dos pipelines falha aqui. Um for attempt in range(3): retry() ingênuo transforma uma tarde ruim em uma tempestade de retries que amplifica bloqueios e queima banda. Uma camada de retry de verdade faz quatro coisas.

1. Classifique antes de tentar de novo. Nem todas as falhas são iguais, e a resposta é diferente para cada uma:

FalhaExemploResposta correta
Transportetimeout, conexão resetadaRetry, a mesma identidade está tudo bem
Rate limit429Recue com força, desacelere o host inteiro
Bloqueio403, CAPTCHA, bloqueio suave com 200Retry em uma nova identidade, não reutilize o IP
Permanente404, 400Não tente de novo, registre e siga em frente

Note o bloqueio suave: um 200 com uma página de CAPTCHA é uma falha. Se você classificar apenas pelo status code, vai contar bloqueios como sucessos e seu pipeline vai silenciosamente se encher de lixo (por que scrapers são bloqueados).

2. Recue com jitter. Backoff exponencial sem jitter sincroniza seus workers em uma manada estrondosa que atinge o alvo em ondas. Sempre adicione aleatoriedade.

3. Mude de identidade nos bloqueios. Tentar de novo um bloqueio no mesmo IP sticky é apenas pedir a mesma rejeição outra vez. Aumente o contador de tentativas para que o id de sessão mude e o gateway te entregue uma saída diferente.

import random, asyncio
async def fetch_with_policy(client, url, job_id, host, max_attempts=4):
for attempt in range(max_attempts):
proxies = proxy_for(job_id, attempt=attempt) # new identity per attempt
try:
r = await client.get(url, proxies=proxies, timeout=30)
kind = classify(r) # ok | ratelimit | block | permanent
if kind == "ok":
return r
if kind == "permanent":
return None # don't waste attempts
if kind == "ratelimit":
await slow_down(host) # widen the host's pacing
except (asyncio.TimeoutError, ConnectionError):
pass # transport: retry
backoff = (2 ** attempt) + random.uniform(0, 1) # jitter, always
await asyncio.sleep(backoff)
await dead_letter(job_id, url) # park it, don't block the pipeline
return None

4. Desista com elegância. Depois de N tentativas, estacione o item em uma dead-letter queue e siga em frente. Retries ilimitados não salvam um job; convertem uma falha em carga sustentada contra um alvo que já está te recusando.

Mais dois padrões que vale a pena construir quando você está em escala: um circuit breaker por host (se a taxa de sucesso desaba, pare de enviar por um tempo em vez de insistir), e itens de trabalho idempotentes, para que um retry seja sempre seguro de executar.

A forma de referência

Junto, o pipeline se parece com isto:

work queue
per-host limiter (semaphore + pacing)
worker → session id (sid) → gateway → target
classify response
├── ok → validate content → emit
├── ratelimit → slow host → backoff + requeue
├── block → new identity → backoff + requeue
└── permanent → record, drop
↓ (after N attempts)
dead-letter queue

Cada seta é uma decisão que você, de outra forma, tomaria implicitamente e mal. Tornada explícita, o sistema se degrada com elegância em vez de desmoronar.

Observabilidade: você não pode operar isso às cegas

Instrumente, no mínimo: taxa de sucesso por host (com validação de conteúdo, não status codes), latência p50/p95, taxa de retry e detalhamento por tipo de falha, e bytes por registro bem-sucedido. Esses quatro indicam onde agir, uma taxa de retry crescente em um host significa apertar a concorrência daquele host; um crescimento em bytes por registro significa que você está puxando payload que não analisa (cortando custos de banda). Os métodos de medição estão em como testar velocidade, taxa de sucesso e precisão de localização de proxy.

Anti-padrões a evitar

  • Construir um rotator de IP em cima de um gateway. Redundante, e com informação pior do que o próprio balanceamento do provedor.
  • Um único limite global de concorrência. Acopla alvos não relacionados; limite por host.
  • Tentar de novo bloqueios no mesmo IP sticky. Mesmo IP, mesma resposta.
  • Backoff sem jitter. Sincroniza workers em ondas.
  • Retries ilimitados. Amplifica bloqueios, queima banda, só adia o inevitável.
  • Tratar 200 como sucesso. Bloqueios suaves são 200s; valide o conteúdo ou seus dados apodrecem silenciosamente.
  • Rotacionar no meio do fluxo. Mudar de IP dentro de uma sessão de múltiplas etapas é um sinal de detecção, rotacione nas fronteiras das unidades.

FAQ

Preciso construir o balanceamento de carga de proxy eu mesmo? Não no nível de IP. Um gateway atribui saídas a partir do pool do lado do servidor, então um rotator de IP ou uma tabela de saúde do seu lado é redundante. Arquitete a camada acima: unidades de identidade, concorrência por host e política de retry.

Quantas requisições concorrentes devo rodar? Depende do alvo, não do seu plano. Defina um limite por host com base no que aquele host tolera, e ajuste cada um de forma independente. Além desse ponto, mais paralelismo produz bloqueios, não throughput.

Um retry deve usar o mesmo IP ou um novo? Depende da falha. Erros de transporte (timeouts) podem tentar de novo na mesma identidade. Bloqueios e CAPTCHAs devem tentar de novo em uma identidade nova, mudando o id de sessão, já que o IP é o que foi recusado.

Como evito que os retries piorem o bloqueio? Classifique as falhas, recue exponencialmente com jitter, limite as tentativas, mande para dead-letter o que não vai ter sucesso, e adicione um circuit breaker por host. Retries ilimitados e não classificados são como um pequeno problema de bloqueio vira um grande.

O que devo monitorar em um pipeline de proxy? Taxa de sucesso por host (com validação de conteúdo), latência p50/p95, taxa de retry com detalhamento por tipo de falha, e bytes por registro bem-sucedido. Esses quatro revelam quase todo problema que vale a pena corrigir.

Conclusão

Em um gateway, balancear a carga do pool não é o seu problema, e fingir que é leva os times a construir a coisa errada. A arquitetura que realmente determina se um pipeline de coleta escala vive em três decisões: o que constitui uma identidade e que trabalho pertence a ela, quanta concorrência cada alvo individual tolera, e como as falhas são classificadas, recuadas, reidentificadas e eventualmente abandonadas. Acerte essas três coisas, instrumente-as, e o pipeline se degrada com elegância sob pressão em vez de desabar.

O gateway cuida do resto. Se você está construindo isso, o gateway residencial te dá rotação e sessões sticky através do username para que sua arquitetura fique no seu código, não em uma camada de gerenciamento de proxy, e a qualidade do pool determina com que frequência esse caminho de retry é exercido (ou não) (reputação de IP). Para considerações em grande escala, veja a melhor rede de proxy residencial para scraping em grande escala, e a página de preços tem os planos por GB para testar o design contra sua própria carga de trabalho.

Pronto para começar?

Experimente os proxies residenciais da Shifter, mais de 205M IPs, mais de 195 países, a partir de $0,75/GB.

Começar