Rust vs Elixir: Concorrência e BEAM vs Tokio | Rust Brasil

Rust vs Elixir: BEAM vs OS threads, Phoenix vs Axum, tolerância a falhas e quando combinar as duas linguagens em 2026.

Introdução

Rust e Elixir são duas linguagens que atraem desenvolvedores apaixonados, mas por razões completamente opostas. Elixir, criada por José Valim em 2011, roda sobre a BEAM (a máquina virtual do Erlang) e é projetada para sistemas concorrentes, distribuídos e tolerantes a falhas. Rust compila para código nativo e foca em performance máxima com segurança de memória.

Enquanto Rust pergunta “como extrair o máximo de performance com segurança?”, Elixir pergunta “como construir sistemas que nunca param de funcionar?”. Essa diferença de filosofia torna a comparação especialmente interessante.

Este artigo é para desenvolvedores web que estão decidindo entre Phoenix e Axum, engenheiros de sistemas que precisam de concorrência pesada e qualquer pessoa que queira entender quando cada linguagem brilha.

Tabela Comparativa

AspectoRustElixir
RuntimeCódigo nativo (sem VM)BEAM (máquina virtual do Erlang)
ParadigmaMulti-paradigma (imperativo + funcional)Funcional (imutabilidade por padrão)
Concorrênciaasync/await + OS threadsProcessos leves BEAM (~2KB cada)
Tolerância a falhasNão nativa (Result + panic)Supervisors + “let it crash”
Hot code reloadNão suportadoNativo na BEAM
Performance CPUMáxima (código nativo)5-20x mais lenta (BEAM interpretada)
LatênciaUltra-baixa, previsívelBaixa e consistente (preemptive scheduling)
TipagemEstática, forteDinâmica, forte (dialyzer para análise estática)
DistribuiçãoManual (gRPC, HTTP)Nativa (distributed Erlang)
Ecossistema webAxum, Actix WebPhoenix (com LiveView)
ImutabilidadeOpt-in (let mut)Por padrão (tudo é imutável)

Modelos de Concorrência: Filosofias Opostas

Elixir: Processos BEAM

Elixir usa processos leves da BEAM — não confunda com processos do sistema operacional. Cada processo BEAM consome apenas ~2KB de memória, e você pode criar milhões deles em uma única máquina:

defmodule ProcessadorPedidos do
  def processar(pedido_id) do
    # Cada pedido é processado em seu próprio processo BEAM
    IO.puts("Processando pedido #{pedido_id}...")
    :timer.sleep(100)  # simula trabalho
    {:ok, pedido_id * 10}
  end
end

defmodule App do
  def executar do
    # Cria 10.000 processos concorrentes trivialmente
    tarefas =
      1..10_000
      |> Enum.map(fn id ->
        Task.async(fn -> ProcessadorPedidos.processar(id) end)
      end)

    resultados =
      tarefas
      |> Enum.map(&Task.await(&1, 5000))

    total =
      resultados
      |> Enum.map(fn {:ok, valor} -> valor end)
      |> Enum.sum()

    IO.puts("Total processado: #{total}")
  end
end

App.executar()

Rust: Async/Await com Tokio

use tokio::task;

async fn processar(pedido_id: u64) -> u64 {
    println!("Processando pedido {pedido_id}...");
    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
    pedido_id * 10
}

#[tokio::main]
async fn main() {
    let mut handles = Vec::new();
    for id in 1..=10_000 {
        handles.push(task::spawn(processar(id)));
    }

    let mut total: u64 = 0;
    for handle in handles {
        total += handle.await.unwrap();
    }

    println!("Total processado: {total}");
}

Ambas as abordagens lidam bem com 10.000 tarefas concorrentes. A diferença:

  • Elixir: processos são preemptivos — a BEAM garante que nenhum processo monopoliza a CPU. Ideal para latência consistente.
  • Rust: tasks são cooperativas — cada task precisa fazer await para devolver controle ao scheduler. Ideal para throughput máximo.

Para se aprofundar em concorrência no Rust, veja nosso tutorial de concorrência.

Tolerância a Falhas: “Let It Crash”

O recurso mais distintivo do Elixir (herdado do Erlang) é a tolerância a falhas via supervisors.

Elixir: Supervisor Trees

defmodule Conexao do
  use GenServer

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: opts[:nome])
  end

  @impl true
  def init(opts) do
    IO.puts("Conexão #{opts[:nome]} iniciada")
    {:ok, %{tentativas: 0}}
  end

  @impl true
  def handle_call(:consultar, _from, estado) do
    # Simula falha intermitente
    if :rand.uniform(10) == 1 do
      raise "Erro de conexão!"
    end
    {:reply, {:ok, "dados"}, estado}
  end
end

defmodule App.Supervisor do
  use Supervisor

  def start_link(_) do
    Supervisor.start_link(__MODULE__, [], name: __MODULE__)
  end

  @impl true
  def init(_) do
    filhos = [
      {Conexao, nome: :db_principal},
      {Conexao, nome: :db_replica},
      {Conexao, nome: :cache},
    ]

    # Se um processo crashar, é reiniciado automaticamente!
    Supervisor.init(filhos, strategy: :one_for_one)
  end
end

Se a conexão do banco crashar, o supervisor reinicia automaticamente apenas aquele processo — o resto do sistema continua funcionando. Esse modelo de “let it crash” torna sistemas Elixir extraordinariamente resilientes.

Rust: Tratamento Explícito de Erros

use std::fmt;

#[derive(Debug)]
struct ErroConexao(String);

impl fmt::Display for ErroConexao {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Erro de conexão: {}", self.0)
    }
}

impl std::error::Error for ErroConexao {}

async fn consultar_com_retry(
    nome: &str,
    tentativas: u32,
) -> Result<String, ErroConexao> {
    for tentativa in 1..=tentativas {
        match consultar_banco(nome).await {
            Ok(dados) => return Ok(dados),
            Err(e) => {
                eprintln!(
                    "[{nome}] Tentativa {tentativa}/{tentativas} falhou: {e}"
                );
                if tentativa < tentativas {
                    tokio::time::sleep(
                        tokio::time::Duration::from_millis(100 * tentativa as u64)
                    ).await;
                }
            }
        }
    }
    Err(ErroConexao(format!("{nome}: todas as tentativas falharam")))
}

async fn consultar_banco(nome: &str) -> Result<String, ErroConexao> {
    // Simula falha intermitente
    if rand::random::<u32>() % 10 == 0 {
        return Err(ErroConexao(format!("timeout em {nome}")));
    }
    Ok("dados".to_string())
}

#[tokio::main]
async fn main() {
    match consultar_com_retry("db_principal", 3).await {
        Ok(dados) => println!("Sucesso: {dados}"),
        Err(e) => eprintln!("Falha final: {e}"),
    }
}

Em Rust, a resiliência é construída manualmente com Result, retries e circuit breakers. Funciona bem, mas requer mais código e disciplina do que o modelo de supervisors do Elixir.

Exemplo Prático: Aplicação Web

Elixir com Phoenix

defmodule AppWeb.TarefaController do
  use AppWeb, :controller

  alias App.Tarefas

  def index(conn, _params) do
    tarefas = Tarefas.listar()
    json(conn, %{tarefas: tarefas})
  end

  def create(conn, %{"titulo" => titulo, "prioridade" => prioridade}) do
    case Tarefas.criar(%{titulo: titulo, prioridade: prioridade}) do
      {:ok, tarefa} ->
        conn
        |> put_status(:created)
        |> json(%{tarefa: tarefa})

      {:error, changeset} ->
        conn
        |> put_status(:unprocessable_entity)
        |> json(%{erros: traduzir_erros(changeset)})
    end
  end

  defp traduzir_erros(changeset) do
    Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
  end
end

Rust com Axum

use axum::{
    extract::State,
    http::StatusCode,
    routing::{get, post},
    Json, Router,
};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};

#[derive(Clone, Serialize)]
struct Tarefa {
    id: u64,
    titulo: String,
    prioridade: String,
}

#[derive(Deserialize)]
struct CriarTarefa {
    titulo: String,
    prioridade: String,
}

type Db = Arc<Mutex<Vec<Tarefa>>>;

async fn listar(State(db): State<Db>) -> Json<Vec<Tarefa>> {
    let tarefas = db.lock().unwrap();
    Json(tarefas.clone())
}

async fn criar(
    State(db): State<Db>,
    Json(dto): Json<CriarTarefa>,
) -> (StatusCode, Json<Tarefa>) {
    let mut tarefas = db.lock().unwrap();
    let id = tarefas.len() as u64 + 1;
    let tarefa = Tarefa {
        id,
        titulo: dto.titulo,
        prioridade: dto.prioridade,
    };
    tarefas.push(tarefa.clone());
    (StatusCode::CREATED, Json(tarefa))
}

#[tokio::main]
async fn main() {
    let db: Db = Arc::new(Mutex::new(Vec::new()));
    let app = Router::new()
        .route("/tarefas", get(listar).post(criar))
        .with_state(db);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:4000")
        .await
        .unwrap();
    axum::serve(listener, app).await.unwrap();
}

Phoenix oferece uma experiência mais “batteries included” com Ecto (ORM), Phoenix LiveView (interfaces reativas sem JavaScript) e PubSub integrado. Axum é mais minimalista, mas significativamente mais rápido.

Comparação de Performance

Benchmarks de Servidor Web

MétricaRust (Axum)Elixir (Phoenix)Diferença
Requisições/s (JSON)~850.000~120.000~7x Rust
Latência p500,2 ms0,5 ms~2,5x Rust
Latência p990,8 ms2,5 ms~3x Rust
Memória idle~5 MB~50 MB~10x Rust
Memória sob carga~25 MB~200 MB~8x Rust
Startup< 5 ms~2.000 msBEAM warmup

Onde Elixir Compensa

Apesar de ser 7x mais lenta em throughput bruto, Elixir tem vantagens práticas:

  • Latência consistente: o scheduler preemptivo da BEAM garante que nenhuma requisição espere muito. Rust pode ter picos de latência se uma task bloquear o scheduler.
  • Hot code reload: atualizar código em produção sem derrubar conexões. Impossível em Rust.
  • Distribuição nativa: conectar nós em cluster é trivial. Em Rust, você precisa implementar manualmente com gRPC ou similar.
  • Concorrência massiva: gerenciar 1 milhão de WebSockets é mais natural com processos BEAM.

Quando Combinar Rust e Elixir

Uma abordagem cada vez mais popular é usar Elixir como orquestrador e Rust nos pontos quentes, via NIFs (Native Implemented Functions) com a biblioteca Rustler:

// native/meu_nif/src/lib.rs
use rustler::{Encoder, Env, NifResult, Term};

#[rustler::nif]
fn processar_imagem(dados: Vec<u8>, largura: u32) -> NifResult<Vec<u8>> {
    // Processamento pesado de imagem em Rust
    let resultado: Vec<u8> = dados
        .chunks(3)
        .flat_map(|pixel| {
            let cinza = (pixel[0] as u32 + pixel[1] as u32 + pixel[2] as u32) / 3;
            vec![cinza as u8; 3]
        })
        .collect();
    Ok(resultado)
}

rustler::init!("Elixir.MeuNif");
defmodule MeuNif do
  use Rustler, otp_app: :minha_app, crate: "meu_nif"

  # Funções implementadas em Rust para performance
  def processar_imagem(_dados, _largura), do: :erlang.nif_error(:not_loaded)
end

# Uso: o melhor dos dois mundos
defmodule ImagemController do
  def processar(conn, %{"imagem" => upload}) do
    dados = File.read!(upload.path)

    # Processamento pesado em Rust, orquestração em Elixir
    resultado = MeuNif.processar_imagem(dados, 1920)

    send_download(conn, {:binary, resultado}, filename: "processada.rgb")
  end
end

Essa combinação é usada em produção por empresas como Discord (que migrou componentes de Elixir para Rust) e pela própria comunidade Elixir com projetos como Explorer (dataframes com Polars/Rust por baixo).

Quando Usar Elixir

Escolha Elixir quando:

  • Sistemas de tempo real: chat, notificações, WebSockets em massa (Phoenix LiveView)
  • Tolerância a falhas é prioridade: sistemas financeiros, telecoms, IoT
  • Distribuição é necessária: clusters de servidores com comunicação nativa
  • Hot code reload importa: sistemas que não podem ter downtime
  • Equipe de tamanho médio: Elixir é produtiva e a comunidade é excelente

Quando Usar Rust

Escolha Rust quando:

  • Throughput bruto é prioridade: processamento de dados em massa, proxies, CDNs
  • Recursos são limitados: serverless, edge computing, containers pequenos
  • Latência ultra-baixa: trading, gaming, processamento de áudio/vídeo
  • CPU-bound: compiladores, compressão, criptografia, processamento de imagem
  • Binários autossuficientes: CLIs e ferramentas sem dependências de runtime

Conclusão e Recomendação

Para aplicações web com requisitos de tempo real e alta disponibilidade, como chat, plataformas de streaming, IoT e sistemas financeiros, Elixir com Phoenix é a escolha ideal. O modelo de concorrência da BEAM, supervisors e hot code reload oferecem uma fundação incomparável para sistemas que não podem parar.

Para serviços de alta performance, processamento de dados e infraestrutura, Rust é claramente superior. A diferença de 7x em throughput e 10x em consumo de memória se traduz em custos de infraestrutura significativamente menores.

A melhor estratégia para muitos projetos é combinar as duas: Elixir para a lógica de aplicação, orquestração e comunicação em tempo real, com Rust via Rustler/NIFs para os componentes CPU-bound. Essa abordagem oferece produtividade, resiliência e performance onde cada uma importa mais.


Veja Também