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
| Aspecto | Rust | Elixir |
|---|---|---|
| Runtime | Código nativo (sem VM) | BEAM (máquina virtual do Erlang) |
| Paradigma | Multi-paradigma (imperativo + funcional) | Funcional (imutabilidade por padrão) |
| Concorrência | async/await + OS threads | Processos leves BEAM (~2KB cada) |
| Tolerância a falhas | Não nativa (Result + panic) | Supervisors + “let it crash” |
| Hot code reload | Não suportado | Nativo na BEAM |
| Performance CPU | Máxima (código nativo) | 5-20x mais lenta (BEAM interpretada) |
| Latência | Ultra-baixa, previsível | Baixa e consistente (preemptive scheduling) |
| Tipagem | Estática, forte | Dinâmica, forte (dialyzer para análise estática) |
| Distribuição | Manual (gRPC, HTTP) | Nativa (distributed Erlang) |
| Ecossistema web | Axum, Actix Web | Phoenix (com LiveView) |
| Imutabilidade | Opt-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
awaitpara 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étrica | Rust (Axum) | Elixir (Phoenix) | Diferença |
|---|---|---|---|
| Requisições/s (JSON) | ~850.000 | ~120.000 | ~7x Rust |
| Latência p50 | 0,2 ms | 0,5 ms | ~2,5x Rust |
| Latência p99 | 0,8 ms | 2,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 ms | BEAM 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
- Tutorial de Concorrência em Rust — Async/await, threads e canais
- Rust vs Go: Qual Escolher em 2026 — Outra linguagem popular para concorrência
- Rust vs Python: Quando Usar Cada Um — Combinando Rust com outra linguagem via FFI
- Tutorial: API REST com Axum — Construa APIs de alta performance
- Receita: Async/Await Básico — Fundamentos de código assíncrono em Rust
- Instalação do Rust — Configure seu ambiente de desenvolvimento