Extração de dados

Lógica de Retry para Proxy Residencial: Lidando com Requisições Falhas e Limitadas por Taxa

Um retry é uma decisão, não um loop. Aqui está a tabela de classificação, o código e as proteções que impedem que os retries transformem um minuto ruim em um dia ruim.

Chris Collins

Chris Collins

28 de agosto de 2026 · 8 min de leitura

Todo scraper precisa de lógica de retry, e a maioria a desenvolve por acidente: um bloco try aqui, um time.sleep ali, um contador de tentativas adicionado na semana em que algo quebrou. O resultado funciona até o dia em que um alvo passa por uma hora ruim, momento em que o caminho de retry causa mais dano do que a falha original causou. O argumento de por que isso acontece está em retry and backoff; este é a implementação.

A ideia organizadora é que um retry é uma decisão tomada a partir de uma falha classificada, não um loop envolto em uma requisição. Acerte a classificação e o resto segue.

Classifique primeiro

Toda falha se encaixa em uma de quatro classes, e cada uma tem exatamente uma resposta correta.

ClasseExemplosResposta
Transitóriatimeout, connection reset, 502, 503, 504retry com backoff
Rate429, Retry-After presenteesperar conforme instruído, mesma rota
Identidadepágina de desafio, 403 persistente, página de bloqueionova sessão, depois retry
Terminal404, 400, 401, 407, falha de parsenão fazer retry, expor o erro

Duas dessas são as que as pessoas erram. Sinais de Rate são instruções sobre ritmo, então a resposta é esperar em vez de trocar de endereços, porque rotacionar para sustentar o mesmo ritmo é exatamente o comportamento que escala um throttle para um bloqueio. Falhas Terminais nunca devem ser retentadas: um 407 significa credenciais ou uma flag de username malformada e será igualmente errado na quinta tentativa, e uma falha de parse é um bug no seu código que um retry reproduz fielmente para sempre.

O quinto caso é o que nunca aparece em um código de status: uma resposta que parece boa e não é. Uma página de desafio, um conjunto de resultados vazio, ou uma listagem truncada servida com um 200 pertence à classe de identidade, mas só se você perceber, o que significa validar o corpo antes de decidir qualquer coisa. Essa verificação é a base de todo o sistema, conforme detecting blocked or fake content.

Classificação em código

import random, time, requests

TRANSIENT = {502, 503, 504}

def classify(resp, exc, is_valid):
    if exc is not None:
        return "transient"                      # timeout, reset, DNS
    if resp.status_code == 429:
        return "rate"
    if resp.status_code in TRANSIENT:
        return "transient"
    if resp.status_code in (403, 401) or looks_like_challenge(resp):
        return "identity"
    if resp.status_code == 200 and not is_valid(resp.text):
        return "identity"                       # soft block: 200 but not our data
    if resp.ok:
        return "ok"
    return "terminal"                           # 404, 400, 407, everything else

looks_like_challenge é específico por alvo e geralmente é uma lista curta de marcadores: uma referência a script de captcha, um título de interstitial conhecido, um corpo bem mais curto que uma página real. Mantenha isso em um único lugar por alvo para que tanto o caminho de retry quanto seu monitoramento usem a mesma definição.

Backoff com jitter, e respeitando o Retry-After

Para a classe transitória, o atraso cresce exponencialmente e deve ser aleatorizado. Atrasos fixos sincronizam seus workers, então cem requisições que falham juntas fazem retry juntas, e a rajada sobrevive ao backoff.

def backoff(attempt, base=1.0, cap=60.0):
    ceiling = min(cap, base * (2 ** attempt))
    return random.uniform(0, ceiling)           # full jitter

Para a classe rate, o alvo pode te dizer exatamente quanto tempo esperar, e essa instrução vence seu próprio cronograma:

def wait_for(resp, attempt):
    ra = resp.headers.get("Retry-After")
    if ra:
        try:
            return min(float(ra), 300)          # honour it, but cap it
        except ValueError:
            pass                                 # HTTP-date form, fall through
    return backoff(attempt, base=2.0)            # rate signals start slower

Juntando tudo

MAX_ATTEMPTS = 4

def fetch(url, country, is_valid, session_id=None):
    sid = session_id
    for attempt in range(MAX_ATTEMPTS):
        proxies = build_proxies(country, sid)     # sid=None means rotate
        resp = exc = None
        try:
            resp = requests.get(url, proxies=proxies, timeout=20)
        except requests.RequestException as e:
            exc = e

        kind = classify(resp, exc, is_valid)

        if kind == "ok":
            return resp
        if kind == "terminal":
            raise TerminalError(url, getattr(resp, "status_code", None))
        if kind == "identity":
            sid = new_session_id() if sid else None   # retire the session
            time.sleep(backoff(attempt))
        elif kind == "rate":
            time.sleep(wait_for(resp, attempt))       # wait, do not rotate
        else:
            time.sleep(backoff(attempt))

    raise Exhausted(url)

Três detalhes ali importam mais do que a estrutura. A função lança exceção em erros terminais em vez de retornar None, então quem chama não pode confundir uma falha com dados vazios. Falhas de identidade substituem a sessão em vez de reutilizá-la, porque a anterior já é conhecida pelo alvo. E falhas de rate deliberadamente não tocam na sessão, mantendo a mesma rota enquanto desaceleram.

Proteções que evitam que os retries se tornem o problema

A lógica por requisição não é suficiente sozinha, porque não tem visão do sistema. Três adições fazem a maior parte do trabalho de proteção.

Um orçamento de retry limita os retries como uma fração do tráfego total para um alvo, digamos dez por cento. Em operação normal você nunca chega perto dele. Quando um alvo quebra de forma ampla, o orçamento se esgota imediatamente e os retries extras simplesmente não acontecem, que é o comportamento desejado, já que retries ajudam com falhas isoladas e prejudicam ativamente durante uma interrupção geral.

Um circuit breaker por alvo para de enviar completamente assim que a taxa de falha ultrapassa um limite, aguarda um cooldown, depois deixa passar um fluxo pequeno para testar a recuperação. Isso protege o alvo do seu acúmulo de tentativas e protege seus endereços de acumularem falhas contra um site que não está respondendo a ninguém.

Um limitador de taxa compartilhado pelo qual os retries também passam. Se os retries contornam seu ritmo, seu caminho de erro se torna uma inundação sem limite exatamente quando o alvo está menos capaz de suportar. Roteie cada tentativa pelo mesmo limitador, conforme rate limiting and throttling.

Os três pertencem ao componente que já vê todas as requisições, o que é o argumento para a proxy manager em vez de código de retry por job.

Idempotência e a dead-letter queue

Duas preocupações práticas fáceis de esquecer.

Retries assumem que a operação é segura de repetir. Para coleta isso é quase sempre verdade, já que buscar uma página duas vezes custa banda e nada mais. Se alguma parte do seu pipeline escreve como efeito colateral de buscar, torne a escrita idempotente, indexada em algo estável, para que um retry não crie um registro duplicado.

E quando uma requisição esgota suas tentativas, não a descarte. Envie-a para uma dead-letter queue e reprocesse-a bem mais tarde, na próxima execução ou após um longo cooldown, em vez de dentro da rajada atual. A maioria dos itens que falham durante um incidente é bem-sucedida por conta própria uma hora depois, e uma fila transforma uma falha definitiva em uma falha adiada sem custo algum.

Observe a taxa de retry

Retries são o aviso mais precoce que você recebe. A proporção de retry, ou seja, retries como fração de requisições por alvo, sobe antes da taxa de sucesso cair, porque um pipeline que faz retry até chegar a um resultado com aparência normal está escondendo um problema em vez de resolvê-lo. Também é custo puro em um produto cobrado por banda, então é ao mesmo tempo uma métrica de saúde e uma métrica de gasto. Coloque isso no dashboard ao lado da taxa de sucesso validada, conforme proxy KPIs e monitoring your pipeline.

Resumo final

Lógica de retry é um classificador seguido de quatro respostas, não um loop com um contador. Valide o corpo para que bloqueios suaves sejam classificados como falhas, depois espere nos sinais de rate sem rotacionar, aposente a sessão em falhas de identidade, use backoff com jitter nas transitórias, e nunca faça retry de um erro terminal. Limite tentativas e atraso. Depois adicione as proteções que uma visão por requisição não pode fornecer: um orçamento de retry para que uma interrupção ampla não vire uma inundação, um circuit breaker por alvo, e um limitador compartilhado que os retries também respeitam. Envie requisições esgotadas para uma dead-letter queue para uma execução posterior, e observe a proporção de retry como seu sinal mais precoce de que algo está mudando.

A metade de failover disso, ter para onde fazer retry de forma limpa, é o que os residential proxies fornecem: um grande pool de endereços reais de qualidade residencial para que uma sessão aposentada seja substituída em vez de reutilizada, com per-GB pricing que torna retries disciplinados diretamente mais baratos do que retries indisciplinados.

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