Desbravando Rust

Performance na prática: Um exemplo de real onde o Axum supera o FastAPI

Por @zejuniortdr em Nov 16, 2025

Qual a stack perfeita para desenvolvimento? Essa é uma pergunta simples porém que trás muitas dúvidas, principalmente para quem está começando e a resposta nem sempre é óbvia: É a que resolve o problema da melhor maneira possível, consumindo menos recursos.

Claro que sempre existirão trade-offs, como por exemplo a curva de aprendizado, que pode ser maior uma determinada linguagem, principalmente quando esta é o Rust devido a sua forma única de trabalhar e as decisões de design da própria linguagem que é bem única em alguns pontos.

Neste post, a ideia é apresentar um exemplo prático onde atráves de um benchmark simples, é possível ver a diferença entre as duas.

O escopo do projeto

Criar uma API Rest com um único endpoint /api/v1/cep/{cep} que receberá via path param e retornará os dados de endereço do CEP informado. Para otimizar a velocidade e reduzir o acoplamento de um serviço externo, ao invés de depender 100% de uma API de terceiros, primeiro a requisição consultará no banco da própria aplicação, e somente em caso de não encontrar o registro, baterá no terceiro para retornar o dado solicitado e persistir no banco para uma consulta futura.

Ambiente do Execução

Stack Python:

Stack Rust

Parâmetros do Teste:

Resultados Quantitativos

Métrica FastAPI (Python) Axum (Rust) Diferença
Requisições/segundo 1.356,92 2.773,98 2,04x
Tempo total de execução 14,7s 7,2s 2,04x
Tempo mediano 28ms 9ms 3,11x
Tempo médio 36ms 18ms 2,00x
Percentil 99 88ms 42ms 2,09x
Percentil 99.5 103ms 51ms 2,02x
Percentil 99.9 2.048ms 1.901ms 1,08x
Desvio padrão 97ms 96ms ~1,00x
Requisições falhadas 0 0 Ambas 100%

Comparativo FastAPI x Axum

A implementação Rust apresentou throughput duas vezes superior e latência mediana três vezes menor em comparação com a implementação Python.​

Análise dos Percentis

Comportamento Típico (Mediana) A mediana de 9ms para Axum versus 28ms para FastAPI indica que, em condições normais de operação, a implementação Rust responde em aproximadamente um terço do tempo.​

Comportamento sob Carga (P99) O percentil 99 mostra que 99% das requisições da implementação Rust completam em até 42ms, enquanto a implementação Python atinge 88ms. Esta diferença é relevante para aplicações com requisitos de SLA rigorosos.​

Casos Extremos (P99.9) Ambas as implementações apresentam spikes similares no percentil 99.9 (~2 segundos). Este comportamento sugere que os outliers não são causados pelos frameworks, mas provavelmente por contenção no PostgreSQL ou latência externa da API ViaCEP.​

Fatores Técnicos de Performance

Modelo de Compilação Python 3.13 executa bytecode através de um interpretador, mesmo com o JIT experimental que oferece melhorias de 2-9%. Rust compila para código de máquina nativo otimizado, eliminando overhead de interpretação.​​

Concorrência e Paralelismo FastAPI com múltiplos workers do Uvicorn distribui carga entre processos, mas cada worker possui seu próprio Global Interpreter Lock (GIL), limitando paralelismo real dentro de cada processo. Rust com Tokio utiliza multi-threading nativo sem restrições de lock global.​

Gerenciamento de Memória Python utiliza garbage collection com pausas imprevisíveis. Rust emprega ownership e borrowing, garantindo segurança de memória em tempo de compilação sem overhead de GC em runtime.​

Ecossistema Assíncrono Ambas as implementações utilizam I/O assíncrono, mas o ecossistema Rust (Tokio + sqlx + reqwest) foi projetado com async como padrão desde a origem, resultando em menor overhead de abstração.​

Considerações sobre sqlx É relevante notar que sqlx não é o driver PostgreSQL mais performático para Rust. Benchmarks do TechEmpower mostram que implementações com tokio-postgres direto alcançam posições significativamente superiores em rankings de performance, devido ao menor overhead de pool de conexões e prepared statements.​

Estrutura dos Handlers

Implementaçao com FastAPI**


@app.get("/api/v1/cep/{cep}")
async def get_cep(cep: str):
    # Limpar CEP (remover hífen)
    cep_limpo = cep.replace("-", "")

    # Validar formato
    if len(cep_limpo) != 8 or not cep_limpo.isdigit():
        raise HTTPException(status_code=400, detail="CEP inválido")

    # Buscar no banco
    async with db_pool.connection() as conn:
        async with conn.cursor(row_factory=dict_row) as cur:
            await cur.execute(
                "SELECT * FROM ceps WHERE cep = %s",
                (cep_limpo,)
            )
            row = await cur.fetchone()

            if row:
                return dict(row)

    # Fallback para ViaCEP
    try:
        response = await http_client.get(f"{VIACEP_URL}/{cep_limpo}/json/")
        response.raise_for_status()
        data = response.json()

        # Verificar se CEP existe
        if "erro" in data:
            raise HTTPException(status_code=404, detail="CEP não encontrado")

        # Salvar no banco
        async with db_pool.connection() as conn:
            async with conn.cursor() as cur:
                await cur.execute("""
                    INSERT INTO ceps (
                        cep, logradouro, complemento, bairro,
                        localidade, uf, ibge, gia, ddd, siafi
                    ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
                    ON CONFLICT (cep) DO NOTHING
                """, (
                    data.get("cep", "").replace("-", ""),
                    data.get("logradouro", ""),
                    data.get("complemento", ""),
                    data.get("bairro", ""),
                    data.get("localidade", ""),
                    data.get("uf", ""),
                    data.get("ibge", ""),
                    data.get("gia", ""),
                    data.get("ddd", ""),
                    data.get("siafi", "")
                ))
                await conn.commit()

        return data

    except httpx.HTTPError as e:
        raise HTTPException(status_code=503, detail=f"Erro ao consultar ViaCEP: {str(e)}")

Características técnicas:

Implementaçao com Axum

pub async fn get_zipcode_handler(
    State(state): State<AppState>,
    Path(cep): Path<String>,
) -> Response {
    dotenv().ok();

    let sanitized_zipcode = match cep_service::validate_cep(&cep) {
        Ok(valid_cep) => valid_cep,
        Err(error_response) => return *error_response,
    };

    let search_zipcode_on_viacep = env::var("SEARCH_ZIPCODE_ON_VIACEP")
        .unwrap_or_else(|_| "false".to_string())
        .to_lowercase()
        == "true";

    let strategy = cep_service::SearchStrategy::WithFallback {
        enable_viacep: search_zipcode_on_viacep
    };

    let result = cep_service::execute_search(&state, &sanitized_zipcode, strategy).await;

    cep_service::search_result_to_response(result, "")
}

Serviço auxiliar

pub async fn execute_search(
        state: &AppState,
        cep: &str,
        strategy: SearchStrategy,
    ) -> SearchResult {
        match strategy {
            SearchStrategy::Single(provider) => match provider {
                Provider::Database => search_in_database(state, cep).await,
                Provider::ViaCep => search_in_viacep(state, cep, Some(false)).await,
            },
            SearchStrategy::WithFallback {
                enable_viacep,
            } => match search_in_database(state, cep).await {
                SearchResult::Found(response) => SearchResult::Found(response),
                SearchResult::Error(err) => SearchResult::Error(err),
                SearchResult::NotFound => {
                    if enable_viacep {
                        match search_in_viacep(state, cep, Some(true)).await {
                            SearchResult::Found(response) => return SearchResult::Found(response),
                            SearchResult::Error(err) => return SearchResult::Error(err),
                            SearchResult::NotFound => {}
                        }
                    }
                    SearchResult::NotFound
                }
            },
        }
    }

pub async fn search_in_database(state: &AppState, cep: &str) -> SearchResult {
        let result = sqlx::query_as!(
            CepResponse,
            r#"
            SELECT
                l.cep,
                COALESCE(l.logradouro, '') as "logradouro!",
                COALESCE(b.nome, '') as "bairro!",
                c.nome as "cidade!",
                e.sigla as "estado!",
                l.created_at,
                'database' as "provider!: String"
            FROM logradouro l
            INNER JOIN cidade c ON l.id_cidade = c.id
            INNER JOIN estado e ON c.id_estado = e.id
            LEFT JOIN bairro b ON l.id_bairro = b.id
            WHERE l.cep = $1
            "#,
            cep
        )
        .fetch_optional(&state.db)
        .await;

        match result {
            Ok(Some(response)) => {
                tracing::info!("Found zipcode in database: {}", cep);
                SearchResult::Found(response)
            }
            Ok(None) => {
                tracing::info!("CEP não encontrado no banco: {}", cep);
                SearchResult::NotFound
            }
            Err(e) => {
                tracing::error!("Erro ao consultar banco de dados: {}", e);
                SearchResult::Error(format!("Erro ao consultar banco de dados: {}", e))
            }
        }
    }

    /// Busca CEP no ViaCep
    pub async fn search_in_viacep(
        state: &AppState,
        cep: &str,
        save_in_db: Option<bool>,
    ) -> SearchResult {
        let viacep_client: ViaCepClient = ViaCepClient::new();
        let must_save = save_in_db.unwrap_or(true);

        match viacep_client.buscar_cep(cep).await {
            Ok(viacep_response) => {
                tracing::info!("CEP {} encontrado no ViaCep", cep);

                if !must_save {
                    return SearchResult::Found(viacep_response);
                }

                if let Err(e) = salvar_cep(state, &viacep_response).await {
                    tracing::error!("Erro ao salvar CEP {} no banco: {}", cep, e);
                }

                SearchResult::Found(viacep_response)
            }
            Err(err) => {
                match err {
                    CepApiError::CepNotFound => {
                        tracing::warn!("CEP {} não encontrado no ViaCep", cep);
                        SearchResult::NotFound
                    }
                    CepApiError::InvalidCep => {
                        tracing::warn!("CEP {} inválido", cep);
                        SearchResult::Error("CEP inválido".to_string())
                    }
                    CepApiError::RequestError(msg) => {
                        tracing::warn!("Erro de request no ViaCep: {}", msg);
                        SearchResult::NotFound // Trata como not found para continuar fallback
                    }
                    CepApiError::HttpError(code) => {
                        tracing::warn!("Erro HTTP no ViaCep: código {}", code);
                        SearchResult::NotFound // Trata como not found para continuar fallback
                    }
                    CepApiError::ParseError(msg) => {
                        tracing::warn!("Erro de parsing no ViaCep: {}", msg);
                        SearchResult::NotFound // Trata como not found para continuar fallback
                    }
                }
            }
        }
    }

Características técnicas:

Casos de Uso para Cada Stack

Quando FastAPI é Adequado

Desenvolvimento Rápido: Para prototipagem e MVPs, onde velocidade de implementação é prioritária. O tempo de desenvolvimento em Python é significativamente menor.​

Ecossistema Rico: Projetos que requerem integração com bibliotecas de machine learning (TensorFlow, PyTorch), data science (pandas, numpy) ou outras ferramentas do ecossistema Python.​

Curva de Aprendizado: Equipes que precisam de produtividade imediata. Desenvolvedores júnior podem contribuir efetivamente com Python em dias, enquanto Rust requer semanas de aprendizado.​

Gargalos Externos: Aplicações onde o tempo de processação é dominado por operações de I/O lentas (queries não otimizadas, APIs externas). Nestes casos, otimização da linguagem tem impacto marginal.​

Quando Axum é Adequado

Alto Volume: Sistemas que precisam processar milhares de requisições por segundo, onde eficiência computacional impacta diretamente custos de infraestrutura.

Requisitos de Latência: APIs públicas ou microserviços internos com SLAs rigorosos de tempo de resposta.

Estabilidade de Longo Prazo: Sistemas que evoluem lentamente e se beneficiam de garantias de type safety e memory safety em compile-time.

Otimização de Custos: Ambientes onde a redução do número de servidores ou instâncias cloud resulta em economia significativa.

Conclusões

Os testes demonstram que Rust com Axum oferece throughput duas vezes superior e latência mediana três vezes menor comparado a Python com FastAPI, em hardware e arquitetura idênticos.​

No entanto, a escolha entre as duas stacks deve considerar fatores além de performance bruta. Python oferece vantagens em velocidade de desenvolvimento, amplitude de ecossistema e curva de aprendizado. Rust se destaca quando escalabilidade, eficiência de recursos e confiabilidade são requisitos críticos.​

A decisão técnica adequada depende do contexto específico de cada projeto: volume esperado de tráfego, requisitos de latência, expertise da equipe, velocidade de iteração necessária e restrições de custo operacional.