← Voltar ao blog

Proxies em Rust reqwest: configuração SOCKS5, pool de clientes e rotação de IP para parser

Analisamos como conectar um proxy SOCKS5 no Rust reqwest, construir um pool de clientes e implementar a rotação de IP para uma extração estável sem bloqueios.

📅18 de outubro de 2026

Qualquer parser que funcione por mais de cinco minutos em sites reais, cedo ou tarde, enfrenta bloqueios por IP. Rust com a biblioteca reqwest oferece excelente desempenho, mas não resolve o problema da rotação de proxies por padrão — isso precisa ser implementado manualmente. Neste artigo, vamos discutir como conectar proxies SOCKS5 e HTTP(S), montar um pool de clientes e organizar a rotação de IP de forma que o parser não falhe após as primeiras respostas 429 e 403.

Por que o parser precisa de proxy e quais são os riscos da sua ausência

Sites como marketplaces, agregadores de preços e redes sociais monitoram atividades anômalas de um único IP: dezenas de requisições por segundo, User-Agent semelhantes, ausência de cookies entre requisições. Assim que o contador de ações suspeitas ultrapassa um limite, o IP entra em uma lista de bloqueio — temporária ou permanente. Para o parser, isso significa a parada total da coleta de dados, até que o IP seja alterado manualmente.

Proxies resolvem esse problema, distribuindo requisições entre dezenas ou centenas de endereços IP. É importante não apenas "substituir o proxy", mas organizar corretamente o pool de conexões e a rotação, caso contrário, você pode enfrentar limites do próprio reqwest (um cliente = um pool de conexões), ou começar a enviar requisições do mesmo proxy com muita frequência. Vamos abordar ambos os problemas em sequência: primeiro a conexão básica, depois a arquitetura do pool.

Para um parser em produção, não é apenas a rotação de IP que importa, mas também a qualidade dos próprios endereços. Se você está fazendo scraping de Wildberries, Ozon ou Avito, IPs de data centers são banidos mais rapidamente do que IPs de usuários reais — mais detalhes na seção sobre a escolha do tipo de proxy.

Configuração básica de proxy HTTP/HTTPS no reqwest

O reqwest suporta proxies através da estrutura Proxy, que deve ser passada para o ClientBuilder. Adicione as dependências no Cargo.toml:

[dependencies]
reqwest = { version = "0.12", features = ["socks", "json"] }
tokio = { version = "1", features = ["full"] }

Um exemplo mínimo com um proxy HTTP que requer autenticação por login e senha:

use reqwest::{Client, Proxy};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let proxy = Proxy::all("http://gate.proxycove.com:8000")?
        .basic_auth("user_login", "user_password");

    let client = Client::builder()
        .proxy(proxy)
        .timeout(std::time::Duration::from_secs(15))
        .build()?;

    let resp = client
        .get("https://httpbin.org/ip")
        .send()
        .await?
        .text()
        .await?;

    println!("Resposta através do proxy: {resp}");
    Ok(())
}

O método Proxy::all direciona tanto o tráfego HTTP quanto HTTPS através do proxy. Se você precisar separar os protocolos, use Proxy::http() e Proxy::https() separadamente — isso é útil se o provedor de proxy escuta diferentes portas para diferentes protocolos.

Conexão de proxy SOCKS5

SOCKS5 opera em um nível mais baixo do que proxies HTTP e suporta conexões TCP sem analisar o conteúdo da requisição — isso é mais rápido e estável para scraping, especialmente se você não está apenas acessando HTTP(S), mas também acessando endpoints WebSocket. Para SOCKS5, certifique-se de habilitar a feature socks no Cargo.toml (já mencionado acima). Exemplo de conexão:

use reqwest::{Client, Proxy};

async fn build_socks5_client(
    host: &str,
    login: &str,
    password: &str,
) -> reqwest::Result<Client> {
    let proxy_url = format!("socks5h://{host}");
    let proxy = Proxy::all(&proxy_url)?.basic_auth(login, password);

    Client::builder()
        .proxy(proxy)
        .timeout(std::time::Duration::from_secs(20))
        .build()
}

Note a esquema socks5h em vez de socks5. A letra "h" no final significa que a resolução DNS do domínio ocorre no lado do servidor proxy, e não localmente. Isso é crítico para a anonimidade: sem isso, seu verdadeiro resolvedor DNS pode "vazar" os domínios solicitados fora do proxy, mesmo que o tráfego esteja passando pelo túnel SOCKS5.

Se o provedor de proxy não exigir autenticação por login/senha (whitelisting de IP), simplesmente não chame basic_auth — especifique no painel do provedor o endereço IP do seu servidor, de onde as requisições estão sendo feitas.

Pool de clientes reqwest para diferentes proxies

Um erro comum é criar um novo Client para cada requisição. Isso é custoso: o reqwest mantém um pool de conexões TCP e sessões TLS, e recriar o cliente elimina todas as vantagens do keep-alive. A abordagem correta é criar um cliente para cada proxy antecipadamente e reutilizá-los de um pool comum.

use reqwest::{Client, Proxy};
use std::sync::Arc;

#[derive(Clone)]
struct ProxyEndpoint {
    url: String,
    login: String,
    password: String,
}

struct ClientPool {
    clients: Vec<Client>,
}

impl ClientPool {
    fn build(endpoints: &[ProxyEndpoint]) -> reqwest::Result<Self> {
        let mut clients = Vec::with_capacity(endpoints.len());

        for ep in endpoints {
            let proxy = Proxy::all(&ep.url)?
                .basic_auth(&ep.login, &ep.password);

            let client = Client::builder()
                .proxy(proxy)
                .pool_max_idle_per_host(4)
                .timeout(std::time::Duration::from_secs(15))
                .build()?;

            clients.push(client);
        }

        Ok(Self { clients })
    }
}

Esse pool é criado uma vez na inicialização do parser. O número de clientes geralmente é igual ao número de endereços proxies únicos em seu pool — se o provedor oferece acesso à rotação no nível do gateway, basta um ou dois clientes, pois a rotação de IP ocorre automaticamente no lado do provedor a cada nova conexão TCP.

Para tarefas com alta paralelismo, envolva o pool em Arc e compartilhe entre as tarefas do tokio — os clientes reqwest são thread-safe e projetados para uso concorrente.

Estratégias de rotação de IP

Existem duas abordagens fundamentalmente diferentes para rotação. A primeira é a rotação do lado do provedor: você se conecta a um endereço de gateway, e o provedor insere um novo IP a cada requisição ou a cada N minutos. A segunda é a rotação do seu lado: você mantém uma lista de proxies estáticos e escolhe qual usar para a próxima requisição.

Se você estiver usando proxies residenciais com rotação automática através do gateway, o código se simplifica ao mínimo — você apenas envia requisições através do mesmo cliente, o provedor muda o IP de saída automaticamente. Mas se você precisar de controle (por exemplo, fixar um IP por sessão por 10 minutos para simular o comportamento de um usuário real), implemente a rotação manualmente:

use rand::seq::SliceRandom;
use std::sync::atomic::{AtomicUsize, Ordering};

struct RotatingPool {
    clients: Vec<Client>,
    counter: AtomicUsize,
}

impl RotatingPool {
    // Round-robin: percorremos os clientes em ordem
    fn next_round_robin(&self) -> &Client {
        let idx = self.counter.fetch_add(1, Ordering::SeqCst) % self.clients.len();
        &self.clients[idx]
    }

    // Escolha aleatória — útil contra análise de padrões
    fn next_random(&self) -> &Client {
        self.clients.choose(&mut rand::thread_rng()).unwrap()
    }
}

Round-robin é adequado quando é importante ter uma carga uniforme em todos os IPs. A escolha aleatória disfarça melhor o padrão de requisições, se o site alvo analisar os intervalos entre acessos de um único endereço. Para tarefas como monitoramento de preços no Ozon ou coleta de anúncios no Avito, na prática, uma combinação é mais eficaz: escolha aleatória de proxies mais um atraso aleatório de 1 a 3 segundos entre requisições do mesmo cliente.

É importante mencionar as sessões sticky. Se o parser passa por um cenário de múltiplas etapas (login → navegação por páginas → ação), o IP não pode ser alterado no meio da sessão — o site notará a mudança de IP durante uma sessão de cookie ativa e pode desconectar ou bloquear a conta. Para tais cenários, fixe um cliente (e, consequentemente, um IP) a uma "sessão" específica do parser durante toda a sua duração.

Tratamento de erros e lógica de retry

Proxies falham de vez em quando: timeouts, 407 (Proxy Authentication Required), interrupções de conexão TCP. O parser deve ser capaz de distinguir entre "proxy quebrado — excluir do pool" e "o site retornou 429 — apenas esperar e mudar o IP". Aqui está um padrão funcional com retry e exclusão de proxies mortos:

use reqwest::StatusCode;
use std::time::Duration;

async fn fetch_with_retry(
    pool: &RotatingPool,
    url: &str,
    max_attempts: u32,
) -> Result<String, String> {
    let mut last_error = String::new();

    for attempt in 0..max_attempts {
        let client = pool.next_random();

        match client.get(url).send().await {
            Ok(resp) if resp.status() == StatusCode::TOO_MANY_REQUESTS => {
                last_error = "429: limite de taxa, mudando IP".into();
                tokio::time::sleep(Duration::from_millis(500 * (attempt as u64 + 1))).await;
                continue;
            }
            Ok(resp) if resp.status().is_success() => {
                return resp.text().await.map_err(|e| e.to_string());
            }
            Ok(resp) => {
                last_error = format!("Status inesperado: {}", resp.status());
            }
            Err(e) if e.is_timeout() || e.is_connect() => {
                last_error = format!("Problema com o proxy: {e}");
                continue;
            }
            Err(e) => {
                last_error = e.to_string();
            }
        }
    }

    Err(format!("Todas as tentativas esgotadas: {last_error}"))
}

Um detalhe importante: o atraso exponencial (500 * attempt) reduz a carga no site alvo durante respostas 429 em massa e ao mesmo tempo diminui o risco de bloqueio total de todo o pool de proxies de uma vez. Para parsers em produção, adicione um contador de erros para cada proxy no pool e exclua automaticamente o endereço se ele gerar 5 ou mais erros consecutivos — isso evitará tentativas inúteis através de um IP já banido.

Qual tipo de proxy escolher para parsing

A escolha do tipo de proxy afeta diretamente quantas requisições bem-sucedidas você obterá antes do bloqueio. IPs de data centers são os mais baratos e rápidos, mas pertencem a faixas conhecidas de provedores de hospedagem — sistemas anti-bot de marketplaces e redes sociais os identificam quase instantaneamente através de bases ASN. IPs residenciais e móveis parecem usuários comuns, portanto, vivem muito mais.

Tipo de proxy Velocidade Risco de bloqueio Quando aplicar
Data Center Alta Alto APIs abertas, sites sem anti-bot
Residenciais Média Baixo Wildberries, Ozon, Avito, redes sociais
Móveis Média Mínimo Instagram, TikTok, anti-bots rigorosos

Para scraping de marketplaces em grandes volumes, um equilíbrio razoável são proxies residenciais: eles não são tão caros em comparação ao tráfego quanto os móveis, mas passam em verificações no nível de "usuário real", que cortam faixas de data centers. Se o parser estiver trabalhando através da API de redes sociais ou simulando um aplicativo móvel, proxies móveis oferecem a menor porcentagem de captchas e bloqueios temporários devido aos IPs, que são vinculados a operadoras de telecomunicações.

Desempenho e dicas de carga

Rust permite manter milhares de requisições concorrentes em uma única instância, mas o provedor de proxy e o site alvo ainda limitam a capacidade real. Algumas regras práticas para um parser em produção:

  • Limite o paralelismo através de tokio::sync::Semaphore — não mais de 20-50 requisições simultâneas por pool de proxy, caso contrário, algumas conexões começarão a falhar devido a timeout.
  • Use pool_idle_timeout no ClientBuilder, para que o reqwest não mantenha conexões mortas através de proxies instáveis.
  • Registre os status HTTP e o tempo de resposta para cada proxy separadamente — assim você verá rapidamente quais endereços começaram a "apodrecer" e precisam ser substituídos.
  • A rotação de User-Agent junto com a rotação de IP reduz significativamente a probabilidade de detecção: altere-os de forma síncrona, e não em ciclos separados.
  • Para parsers de longa duração, adicione um health-check: a cada poucos minutos, envie uma requisição de teste para https://httpbin.org/ip através de cada proxy e exclua os que não respondem.

Preste atenção especial às impressões TLS (JA3/JA3S). O reqwest, por padrão, usa rustls ou nativetls dependendo das features de compilação, e ambos têm diferentes impressões de handshake TLS. Alguns sistemas anti-bot (por exemplo, em grandes marketplaces) analisam especificamente a impressão TLS, e não apenas o IP — portanto, mesmo o proxy ideal não ajudará se o próprio cliente se apresentar com um handshake TLS não padrão. Se você enfrentar bloqueios com proxies "limpos", verifique as features do reqwest e experimente a feature native-tls em vez de rustls, ou vice-versa.

Conclusão

Proxies em Rust reqwest são configurados rapidamente — algumas linhas com Proxy::all ou esquema SOCKS5 socks5h://. O trabalho principal começa a seguir: construir um pool de clientes, escolher uma estratégia de rotação (round-robin, aleatória ou sessões sticky) e um tratamento de erros adequado com a exclusão de endereços não funcionais do pool. Sem isso, mesmo o parser mais rápido em Rust encontrará bloqueios após alguns minutos de operação.

Se seu parser trabalha com marketplaces ou redes sociais, onde a proteção anti-bot analisa não apenas a frequência das requisições, mas também a origem do IP, comece com proxies residenciais — eles oferecem o melhor equilíbrio entre preço e durabilidade das sessões. Para objetivos mais rigorosos, como Instagram e TikTok, vale a pena testar IPs móveis, enquanto para tarefas simples sem anti-bot, proxies de data centers rápidos e baratos são mais do que suficientes.