Como processar consultas de CPF de forma assíncrona em Rust com Tokio

Processe múltiplas consultas de CPF de forma assíncrona em Rust usando Tokio para máxima performance e throughput.

Lucas Vieira
Lucas Vieira
··6 min de leitura
Como processar consultas de CPF de forma assíncrona em Rust com Tokio

Processar consultas de CPF de forma assíncrona em Rust com Tokio permite executar dezenas de chamadas à API da CPFHub.io em paralelo, aproveitando o modelo de concorrência sem threads pesadas do SO. Com semáforos do Tokio você controla quantas requisições correm simultaneamente, distribuindo o volume ao longo do tempo. Esgotar os 50 créditos do plano Grátis faz a API devolver HTTP 403, "Limite de créditos excedido", e pausar novas consultas. Nos planos pagos, havendo excedente, ele entra na fatura seguinte conforme a página de preços. O ritmo por minuto, se ultrapassado, gera HTTP 429 com Retry-After.

Introdução

Quando é necessário processar um grande volume de consultas de CPF, a execução sequencial pode se tornar um gargalo significativo. O Tokio, runtime assíncrono mais popular de Rust, permite executar múltiplas consultas simultaneamente sem o overhead de threads do sistema operacional.


Configurando o ambiente assíncrono

Adicione as dependências necessárias para processamento assíncrono:

// Cargo.toml
[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
futures = "0.3"
CratePapel no processamento
tokioRuntime assíncrono e primitivas de concorrência
reqwestCliente HTTP assíncrono
futuresUtilitários para trabalhar com futures e streams
serdeDesserialização da resposta JSON

Para a documentação completa do Tokio e seu modelo de concorrência, consulte o guia oficial do Tokio.


Criando o cliente compartilhado

Utilize um reqwest::Client compartilhado para reutilizar conexões TCP entre as consultas:

use reqwest::header::{HeaderMap, HeaderValue};
use serde::Deserialize;
use std::sync::Arc;

#[derive(Debug, Deserialize)]
pub struct CpfResponse {
    pub success: bool,
    pub data: CpfData,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CpfData {
    pub cpf: String,
    pub name: String,
    pub name_upper: String,
    pub gender: String,
    pub birth_date: String,
    pub day: String,
    pub month: String,
    pub year: String,
}

pub struct CpfClient {
    client: reqwest::Client,
    api_key: String,
}

impl CpfClient {
    pub fn new(api_key: String) -> Arc<Self> {
    let client = reqwest::Client::builder()
    .pool_max_idle_per_host(20)
    .timeout(std::time::Duration::from_secs(10))
    .build()
    .expect("Falha ao criar cliente HTTP");

    Arc::new(Self { client, api_key })
    }

    pub async fn consultar(&self, cpf: &str) -> Result<CpfResponse, String> {
    let mut headers = HeaderMap::new();
    headers.insert(
    "x-api-key",
    HeaderValue::from_str(&self.api_key).map_err(|e| e.to_string())?,
    );

    let url = format!("https://api.cpfhub.io/cpf/{}", cpf);

    let response = self.client
    .get(&url)
    .headers(headers)
    .send()
    .await
    .map_err(|e| format!("Erro na requisição: {}", e))?;

    response.json::<CpfResponse>()
    .await
    .map_err(|e| format!("Erro ao desserializar: {}", e))
    }
}

Processamento em lote com join_all

Para processar um lote de CPFs em paralelo, utilize futures::future::join_all:

use futures::future::join_all;

async fn processar_lote(
    client: &Arc<CpfClient>,
    cpfs: Vec<String>,
) -> Vec<(String, Result<CpfResponse, String>)> {
    let futures: Vec<_> = cpfs.into_iter().map(|cpf| {
    let client = Arc::clone(client);
    async move {
    let resultado = client.consultar(&cpf).await;
    (cpf, resultado)
    }
    }).collect();

    join_all(futures).await
}

Controlando concorrência com semáforo

Use um semáforo do Tokio para limitar a concorrência e distribuir o volume de consultas de forma controlada:

use tokio::sync::Semaphore;

async fn processar_com_limite(
    client: &Arc<CpfClient>,
    cpfs: Vec<String>,
    max_concurrent: usize,
) -> Vec<(String, Result<CpfResponse, String>)> {
    let semaphore = Arc::new(Semaphore::new(max_concurrent));
    let mut handles = Vec::new();

    for cpf in cpfs {
    let client = Arc::clone(client);
    let semaphore = Arc::clone(&semaphore);

    let handle = tokio::spawn(async move {
    let _permit = semaphore.acquire().await
    .expect("Semáforo fechado");

    let resultado = client.consultar(&cpf).await;
    (cpf, resultado)
    });

    handles.push(handle);
    }

    let mut resultados = Vec::new();
    for handle in handles {
    if let Ok(resultado) = handle.await {
    resultados.push(resultado);
    }
    }

    resultados
}

Exemplo completo com relatório

Monte o programa que processa um lote e gera um relatório dos resultados:

#[tokio::main]
async fn main() {
    let api_key = std::env::var("CPFHUB_API_KEY")
    .expect("Defina CPFHUB_API_KEY");

    let client = CpfClient::new(api_key);

    let cpfs: Vec<String> = vec![
    "12345678900", "98765432100", "11122233344",
    "55566677788", "99988877766",
    ].into_iter().map(String::from).collect();

    let total = cpfs.len();
    println!("Processando {} CPFs com limite de 3 simultâneos...", total);

    let inicio = std::time::Instant::now();
    let resultados = processar_com_limite(&client, cpfs, 3).await;
    let duracao = inicio.elapsed();

    let mut sucessos = 0;
    let mut falhas = 0;

    for (cpf, resultado) in &resultados {
    match resultado {
    Ok(r) if r.success => {
    println!("[OK] {} -> {}", cpf, r.data.name);
    sucessos += 1;
    }
    Ok(_) => {
    println!("[--] {} -> Não encontrado", cpf);
    falhas += 1;
    }
    Err(e) => {
    println!("[ER] {} -> {}", cpf, e);
    falhas += 1;
    }
    }
    }

    println!("\nRelatório:");
    println!(" Total: {}", total);
    println!(" Sucessos: {}", sucessos);
    println!(" Falhas: {}", falhas);
    println!(" Tempo: {:.2?}", duracao);
}

Perguntas frequentes

A API CPFHub.io retorna HTTP 429 quando muitas consultas assíncronas são disparadas ao mesmo tempo?

O teto por minuto responde HTTP 429 com Retry-After e não se confunde com a cota mensal. No Grátis, acabar os 50 créditos pausa as consultas com HTTP 403 ("Limite de créditos excedido"). Se o plano pago tiver excedente, o valor por crédito está na página de preços. O semáforo do Tokio limita conexões e memória locais; manter a concorrência baixa também reduz a chance de estourar o limite por minuto (HTTP 429).

Por que usar Arc<CpfClient> em vez de criar um cliente por tarefa assíncrona?

O reqwest::Client reutiliza um pool de conexões TCP, o que reduz significativamente a latência e o consumo de recursos. Criar um cliente por tarefa desperdiça esse pool e pode esgotar as portas do SO em volume alto. Com Arc, todas as tasks compartilham o mesmo cliente de forma segura entre threads.

Qual é a diferença prática entre join_all e o semáforo com tokio::spawn?

join_all dispara todas as futures simultaneamente, sem limite de concorrência — adequado para lotes pequenos. O semáforo com tokio::spawn limita quantas consultas rodam ao mesmo tempo, o que é mais adequado para lotes grandes onde você quer controlar o uso de memória e conexões. Para a maioria dos casos de produção com a CPFHub.io, use o semáforo.

Como medir o throughput real de consultas assíncronas de CPF em Rust?

Use std::time::Instant::now() antes e elapsed() depois do lote, como no exemplo completo deste artigo. Para métricas mais detalhadas em produção, considere integrar com a crate metrics ou exportar para Prometheus — veja o artigo sobre observabilidade na integração com API de CPF.


Conclusão

O processamento assíncrono com Tokio permite escalar consultas de CPF de forma eficiente, mantendo controle sobre a concorrência e o consumo de recursos. A combinação de semáforos, futures e o cliente HTTP compartilhado resulta em alto throughput com uso mínimo de memória e conexões. No gratuito, zerar os 50 créditos do mês interrompe as consultas com HTTP 403 ("Limite de créditos excedido"). Nos planos pagos o excedente não é um valor único: ele varia e está publicado na página de preços. O limite por minuto responde HTTP 429 com Retry-After.

Cadastre-se em cpfhub.io — 50 consultas mensais gratuitas, sem cartão de crédito — e comece a processar consultas de CPF em escala com Rust e Tokio.

CPFHub.io

Pronto para integrar a API?

50 créditos gratuitos para testar agora. Sem cartão de crédito. Acesso imediato à documentação.

WhatsAppFale conosco via WhatsApp