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_timeoutno 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.