← Voltar ao blog

5 erros no middleware de proxy do Scrapy que queimam tráfego de proxy desnecessariamente

Cinco erros comuns no middleware Scrapy que fazem com que o tráfego de proxies seja desperdiçado e o parser receba bans. Com exemplos de código e soluções prontas.

📅29 de setembro de 2026

O parser no Scrapy falha por timeout, o pool de proxies é consumido muito mais rápido do que o esperado, e os logs estão cheios de respostas 407 e 403 — uma cena familiar? Em 90% dos casos, o problema não está nos próprios proxies, mas na forma como o DownloaderMiddleware foi escrito. Vamos analisar cinco dos erros mais comuns no middleware que transformam tráfego caro em requisições inúteis e mostrar como corrigi-los com código.

Erro 1: rotação primitiva sem considerar o estado do proxy

A construção mais comum que pode ser encontrada em tutoriais é random.choice(PROXY_LIST) dentro de process_request. O problema é que essa rotação não sabe qual proxy acabou de ser banido e qual ainda está ativo. Como resultado, o parser continua enviando requisições através de um IP já bloqueado, recebe 403/429, faz retry — e novamente escolhe o mesmo endereço, pois a seleção é aleatória e não exclui os nós "ruins".

A abordagem correta é manter o estado de cada proxy: número de requisições bem-sucedidas, número de erros, tempo do último uso. Aqui está uma versão mínima funcional:

import random
import time

class ProxyPool:
    def __init__(self, proxies):
        self.proxies = {p: {"fails": 0, "last_used": 0, "banned_until": 0} for p in proxies}

    def get_proxy(self):
        now = time.time()
        available = [
            p for p, state in self.proxies.items()
            if state["banned_until"] < now
        ]
        if not available:
            # se todos estão banidos, reiniciamos o ban mais "antigo"
            available = list(self.proxies.keys())
        return random.choice(available)

    def mark_fail(self, proxy, cooldown=300):
        self.proxies[proxy]["fails"] += 1
        self.proxies[proxy]["banned_until"] = time.time() + cooldown

    def mark_success(self, proxy):
        self.proxies[proxy]["fails"] = 0
        self.proxies[proxy]["last_used"] = time.time()

Esse pool exclui IPs banidos durante o tempo de "cooldown" e os retorna ao uso posteriormente. Isso já reduz o consumo de tráfego drasticamente, pois você não está atacando o mesmo nó bloqueado repetidamente.

Erro 2: tratamento incorreto de retry e códigos de status

O segundo erro típico é usar o RetryMiddleware padrão do Scrapy sem modificação. Por padrão, ele tenta novamente a requisição no mesmo proxy que falhou, a menos que você tenha inserido explicitamente a troca de proxy em process_exception. Como resultado, obtemos a clássica situação: 3 retries, 3 bans, a requisição ainda falha, e o tráfego já foi gasto.

O segundo ponto é que nem todos os códigos de status precisam ser retriados da mesma forma. 429 (Too Many Requests) requer uma pausa e troca de IP, 403 geralmente significa ban de um proxy específico (necessita de troca imediata), enquanto 5xx — frequentemente é um problema temporário do lado do servidor, podendo ser retriado no mesmo proxy. Se você tratar tudo da mesma forma, o middleware ou queima os proxies de forma muito agressiva ou espera muito tempo onde era necessário trocar o IP imediatamente.

class SmartRetryMiddleware:
    def __init__(self, pool):
        self.pool = pool

    def process_response(self, request, response, spider):
        proxy = request.meta.get("proxy")
        if response.status in (403, 407):
            if proxy:
                self.pool.mark_fail(proxy, cooldown=600)
            new_request = request.copy()
            new_request.meta["proxy"] = self.pool.get_proxy()
            new_request.dont_filter = True
            return new_request

        if response.status == 429:
            if proxy:
                self.pool.mark_fail(proxy, cooldown=120)
            new_request = request.copy()
            new_request.meta["proxy"] = self.pool.get_proxy()
            new_request.dont_filter = True
            return new_request

        if proxy:
            self.pool.mark_success(proxy)
        return response

Aqui é importante diferenciar o cooldown por tipo de erro: ban rígido (403/407) — pausa longa, limite de taxa (429) — pausa curta. Isso economiza dezenas de porcentagens de tráfego em longas execuções.

Erro 3: ausência de sessões sticky para sites com autenticação

Se o parser está trabalhando com um site que possui login, carrinho, paginação com manutenção de estado ou captcha com verificação por IP — a troca de proxies a cada requisição quebra a sessão. O site vê que a requisição nº 1 veio de um IP, enquanto a requisição nº 2 (dentro da mesma sessão de cookies) veio de outro, e isso aciona a proteção contra bots instantaneamente, mesmo que ambos os IPs sejam "limpos".

A solução é vincular um proxy a uma sessão lógica (por exemplo, a uma conta específica ou a uma cadeia de requisições dentro de um mesmo domínio) por um tempo fixo, em vez de trocá-lo a cada requisição. Isso é chamado de sessão sticky.

class StickySessionMiddleware:
    def __init__(self, pool, ttl=600):
        self.pool = pool
        self.ttl = ttl
        self.sessions = {}  # session_id -> (proxy, expires_at)

    def process_request(self, request, spider):
        session_id = request.meta.get("session_id")
        if not session_id:
            return

        now = time.time()
        session = self.sessions.get(session_id)

        if session and session[1] > now:
            request.meta["proxy"] = session[0]
        else:
            proxy = self.pool.get_proxy()
            self.sessions[session_id] = (proxy, now + self.ttl)
            request.meta["proxy"] = proxy

Para tarefas que exigem estabilidade de IP durante toda a sessão — autenticação, trabalho com painel de controle, formulários de múltiplas etapas — os proxies residenciais com suporte a sessões são os mais adequados: eles permitem manter o mesmo IP de saída por vários minutos ou horas, e depois trocá-lo de forma controlada, em vez de aleatoriamente a cada requisição.

Erro 4: transmissão incorreta da autenticação do proxy

O quarto erro é técnico, mas aparece em quase todos os segundos projetos. Os desenvolvedores transmitem o login e a senha do proxy diretamente na URL do tipo http://user:pass@ip:port através de request.meta["proxy"]. Isso funciona na maioria dos casos, mas ao usar proxies através de um túnel HTTPS ou ao trabalhar com alguns provedores, esse método de autenticação é processado incorretamente pelo padrão HttpProxyMiddleware, e as requisições falham com 407 Proxy Authentication Required, mesmo que as credenciais estejam corretas.

Um método mais confiável é transmitir o cabeçalho Proxy-Authorization explicitamente, codificado em base64:

import base64

class ProxyAuthMiddleware:
    def process_request(self, request, spider):
        proxy = request.meta.get("proxy")
        if not proxy:
            return

        # proxy sem credenciais na URL
        request.meta["proxy"] = proxy

        user = request.meta.get("proxy_user")
        password = request.meta.get("proxy_pass")
        if user and password:
            credentials = f"{user}:{password}"
            encoded = base64.b64encode(credentials.encode()).decode()
            request.headers["Proxy-Authorization"] = f"Basic {encoded}"

Essa abordagem funciona de forma mais estável ao escalar para centenas de requisições paralelas e não depende de como uma versão específica da biblioteca analisa URLs com credenciais embutidas. Isso é especialmente crítico ao trabalhar com proxies móveis, onde a autenticação muitas vezes está ligada a uma lista de IPs permitidos ou a uma verificação rigorosa de cabeçalhos.

Erro 5: falta de monitoramento e registro de bans

O último e, possivelmente, o erro mais caro em termos de consequências é a falta de registro de quais proxies estão sendo banidos, com que frequência e em quais domínios. Sem esses dados, é impossível entender o que exatamente está consumindo o tráfego: se o pool de proxies se esgotou em um site específico ou se o problema está no próprio parser (requisições muito frequentes, falta de delays, cabeçalhos suspeitos).

O conjunto mínimo de métricas que vale a pena registrar no middleware:

  • Número de requisições para cada proxy durante a sessão de parsing
  • Número e códigos de erros (403, 407, 429, 5xx) para cada proxy
  • Tempo de vida do proxy até o primeiro ban
  • Domínios onde os bans ocorrem com mais frequência
import logging
import json

logger = logging.getLogger("proxy_stats")

class ProxyStatsMiddleware:
    def __init__(self):
        self.stats = {}

    def process_response(self, request, response, spider):
        proxy = request.meta.get("proxy", "unknown")
        domain = request.url.split("/")[2]
        key = f"{proxy}|{domain}"

        entry = self.stats.setdefault(key, {"requests": 0, "errors": 0})
        entry["requests"] += 1
        if response.status in (403, 407, 429):
            entry["errors"] += 1

        if entry["requests"] % 50 == 0:
            logger.info(json.dumps(self.stats))

        return response

Sem essas estatísticas, quaisquer tentativas de "otimizar" o middleware se resumem a adivinhações. Com isso, você vê claramente: se, por exemplo, um domínio específico banir 80% do pool de proxies nas primeiras 10 requisições — o problema não está nos proxies, mas no padrão das requisições (sem rotação de User-Agent, frequência muito alta, ausência de delays entre requisições).

Exemplo funcional de middleware completo

Vamos juntar tudo no settings.py — a ordem do middleware é crítica, pois depende da ordem em que as verificações são aplicadas:

DOWNLOADER_MIDDLEWARES = {
    "myproject.middlewares.ProxyAuthMiddleware": 350,
    "myproject.middlewares.StickySessionMiddleware": 400,
    "myproject.middlewares.SmartRetryMiddleware": 550,
    "myproject.middlewares.ProxyStatsMiddleware": 900,
}

RETRY_ENABLED = False  # desativamos o retry padrão, usamos o nosso
DOWNLOAD_TIMEOUT = 15
CONCURRENT_REQUESTS_PER_DOMAIN = 8

Preste atenção em RETRY_ENABLED = False — isso é fundamental, caso contrário, o mecanismo de retry embutido do Scrapy entrará em conflito com sua lógica de troca de proxies, e as requisições começarão a ser duplicadas ou retriadas duas vezes. Também é importante limitar CONCURRENT_REQUESTS_PER_DOMAIN — uma paralelidade muito alta em um único domínio, mesmo com rotação de proxies, parece suspeita para sistemas anti-bots.

Qual tipo de proxy escolher para Scrapy

O middleware resolve metade do problema, a outra metade é a escolha correta do próprio pool de proxies para a tarefa. Abaixo está uma comparação para cenários típicos de parsing.

Tipo de proxy Quando usar Prós Contras
Proxies de datacenter Parsing em massa de páginas abertas sem proteção anti-bots rigorosa Alta velocidade, baixo custo de tráfego Fácil de detectar, frequentemente em blacklist
Proxies residenciais Parsing de marketplaces, sites com proteção JS, autenticação IPs reais, baixo percentual de bans, suporte a sessões sticky Velocidade inferior em comparação com DC
Proxies móveis Parsing de versões móveis de sites e APIs com proteção anti-bots rigorosa Máxima confiança por parte dos sites-alvo Custo de tráfego mais alto

Regra prática: para parsing de páginas estáticas sem proteção forte, proxies de datacenter baratos em conjunto com um middleware bem elaborado deste artigo são adequados. Se o site utiliza Cloudflare, PerimeterX, DataDome ou sistemas semelhantes — os IPs de datacenter serão banidos quase imediatamente, e aqui já é mais vantajoso passar diretamente para um pool residencial, economizando tempo na depuração do middleware.

Checklist antes de colocar o parser em produção

  • O middleware exclui proxies banidos durante o cooldown, em vez de escolhê-los aleatoriamente
  • A lógica de retry diferencia tipos de erros (403/407 vs 429 vs 5xx) com diferentes cooldowns
  • Para tarefas de sessão, é utilizada a vinculação sticky do proxy, em vez de troca a cada requisição
  • A autenticação do proxy é transmitida através do cabeçalho Proxy-Authorization, e não apenas pela URL
  • Estatísticas de bans por domínios e proxies são mantidas para diagnóstico de problemas
  • O RetryMiddleware padrão do Scrapy está desativado para não entrar em conflito com a lógica personalizada
  • A concorrência é limitada a valores razoáveis, e não definida no máximo

Conclusão

A maioria dos problemas com o consumo de tráfego de proxy no Scrapy é resolvida não pela compra de um pool maior de IPs, mas pela correção da lógica do middleware: separação de erros por tipos, sessões sticky para cenários complexos, autenticação correta e monitoramento constante de bans. O código deste artigo pode ser usado como base e adaptado para um projeto específico — a estrutura permanece funcional tanto para pequenos parsers quanto para instalações distribuídas do Scrapy-Cluster.

Se o middleware já está configurado corretamente, mas os bans ainda ocorrem com muita frequência — provavelmente, o problema está na qualidade do próprio pool de IPs. Para parsing de sites com proteção anti-bots avançada, vale a pena experimentar proxies residenciais com suporte a sessões — eles reduzem significativamente a porcentagem de ativação da proteção em comparação com endereços de datacenter, mantendo a mesma lógica de middleware.