Trench Benchmark: Avaliando Elixir/BEAM e Bun contra Node.js em Produção no Linux Mint#
O mercado de tecnologia vivencia ciclos frequentes que promovem novos runtimes sob a promessa de latência mínima e concorrência ilimitada. Em ambientes corporativos de alta disponibilidade, manter uma infraestrutura estável exige validações rigorosas. A malha operacional baseia-se em uma stack consolidada: Node.js, pnpm, Prisma ORM e instâncias PostgreSQL. Contudo, o amadurecimento do Elixir (Phoenix) e a eficiência de execução trazida pelo Bun motivaram a realização de uma auditoria de desempenho e compatibilidade técnica.
Esta análise documenta uma comparação estruturada entre estes runtimes, detalhando seus modelos de execução, arquitetura de rede, segurança, observabilidade e métricas de desempenho sob sistemas operacionais Linux Mint.
1. A viabilidade da transição do Node.js#
A decisão de descolar de um ecossistema amplamente adotado como o Node.js não se resume à velocidade de processamento pura. Ela envolve custos de manutenção, treinamento de equipe e reescrevabilidade de código. Enquanto o ecossistema JavaScript possui a maior base de desenvolvedores e bibliotecas do mundo, problemas crônicos relacionados ao consumo de memória em alta escala e ao bloqueio do loop de eventos levam arquitetos a avaliar alternativas como a máquina virtual do Erlang (BEAM) ou o runtime otimizado do Bun.
2. O escopo de concorrência: a máquina virtual erlang (BEAM)#
O ecossistema Elixir apoia-se na robustez da BEAM (Erlang Virtual Machine), que gerencia a execução através do Modelo de Atores. Em vez de threads pesadas do sistema operacional, a BEAM gerencia processos Erlang extremamente leves que consomem cerca de 1 KB a 2 KB de memória cada no momento da criação.
Em termos práticos, em vez de lidar com concorrência compartilhando memória (o que exige locks complexos e expõe o sistema a race conditions), os processos Erlang comunicam-se exclusivamente por troca de mensagens assíncronas. A tolerância a falhas é estruturada segundo a filosofia let it crash:
- O programador não tenta capturar de maneira defensiva todas as exceções possíveis;
- Cada processo isolado executa sua tarefa e, se ocorrer um erro crítico, ele é simplesmente encerrado;
- Um processo Supervisor monitora a árvore de supervisão (Supervision Tree) e reinicia o processo filho aplicando estratégias predefinidas (como
one_for_oneouone_for_all).
Isso garante que um erro em uma requisição isolada não afete a estabilidade global do runtime do servidor.
3. Phoenix liveview e o fluxo state-of-the-art#
O Phoenix LiveView altera o paradigma tradicional de desenvolvimento web unificando o controle do estado no servidor e transmitindo atualizações dinâmicas ao navegador através de conexões WebSocket persistentes.
- Em vez de carregar grandes bibliotecas Single Page Application (SPA) no cliente, o servidor envia apenas um payload HTML inicial básico;
- Uma vez estabelecida a conexão WebSocket, qualquer alteração de estado no backend computa a diferença no template e envia de volta ao navegador apenas pequenos fragmentos de strings (HTML diffs);
- O cliente, utilizando uma biblioteca leve JavaScript nativa, renderiza as alterações na árvore DOM.
Trade-offs operacionais: Embora essa abordagem elimine a complexidade de compilação de pacotes (bundles) grandes no cliente, ela transfere a carga de processamento e persistência de memória para o servidor. Cada conexão WebSocket ativa mantém um processo stateful em memória na BEAM, exigindo maior provisionamento de RAM em cenários de milhões de conexões simultâneas.
4. O custo do débito arquitetural na refatoração#
Migrar de um sistema baseado em Node.js para Elixir impõe um choque de paradigmas computacionais. Sair do modelo imperativo ou orientado a objetos e adotar a programação funcional pura exige a reescrita completa do código.
- Estruturas de iteração tradicionais do JavaScript, como loops
forewhile, não existem em Elixir; - A iteração sobre conjuntos de dados é realizada obrigatoriamente através de chamadas recursivas ou utilizando funções utilitárias de mapeamento e redução do módulo
Enum(comoEnum.map/2ouEnum.reduce/3); - Os dados são estritamente imutáveis, o que significa que qualquer modificação gera uma nova estrutura na memória, exigindo atenção ao coletor de lixo (Garbage Collector) sob tráfego severo.
5. Ecto vs. prisma e gerenciamento de dependências#
A camada de acesso a dados e persistência também difere radicalmente entre as stacks:
- O Prisma opera como um ORM (Object-Relational Mapping) declarativo clássico no ecossistema Node/Bun, utilizando engines compiladas em Rust para realizar as queries;
- O Ecto em Elixir não segue o padrão Active Record. Ele atua como um wrapper de banco de dados e mapeador de esquemas (schema mapper). O controle de mutações de dados é gerenciado por Changesets (que validam e moldam os dados antes de enviá-los ao banco), o que desacopla a representação da tabela das transações de escrita.
No gerenciamento de dependências, enquanto o pnpm no ecossistema JavaScript acelera instalações usando um repositório centralizado de arquivos em cache e criando hard links/symlinks sob o diretório node_modules/, o Mix (ferramenta de build do Elixir) gerencia dependências nativamente por meio do repositório Hex, baixando e compilando pacotes isolados diretamente na pasta de dependências do projeto deps/ sem o uso de estruturas de links simbólicos.
6. Estratégia de migração: o padrão strangler fig#
Substituir um monolito Node.js ativo de uma só vez é financeiramente inviável e introduz sérios riscos de interrupção comercial. A abordagem técnica recomendada para mitigar esse risco é aplicar o padrão Strangler Fig:
Ao isolar rotas e serviços por trás de um API Gateway (como Nginx ou HAProxy), as APIs legadas em Node.js continuam processando as requisições padrão enquanto novas rotas críticas ou conexões de alta concorrência baseadas em WebSockets são criadas em Elixir de forma incremental.
7. O modelo de execução do Node.js: event loop e concorrência#
O modelo de concorrência do Node.js é baseado no Event Loop executado em uma única thread principal (Single-Threaded para execução de código JavaScript). Contudo, a afirmação de que o Node.js é inteiramente single-thread é conceitualmente incompleta.
- O Node.js utiliza a biblioteca libuv escrita em C/C++ para gerenciar operações assíncronas do sistema operacional;
- A
libuvgerencia um pool de threads do sistema operacional (Thread Pool) configurado por padrão para 4 threads paralelas; - Operações pesadas como acesso ao sistema de arquivos (I/O), criptografia (Crypto) e compressão de dados (Zlib) são delegadas ao Thread Pool, enquanto a thread principal de JavaScript continua livre para processar o loop de eventos.
Se a thread de JavaScript principal executar um cálculo de alta CPU bloqueante (CPU-bound task), o Event Loop congelará, resultando em latência e timeouts nas conexões HTTP ativas.
8. A aceleração do Bun e a máquina javascriptcore#
Diferente do Node.js e do Deno, que utilizam o motor V8 do Google, o Bun foi desenvolvido utilizando o motor JavaScriptCore (JSC) do WebKit, mantido pela Apple.
- O JavaScriptCore foi projetado visando velocidade de inicialização acelerada ( startup time) e menor pegada de memória RAM física;
- Ele conta com três otimizadores JIT (Just-In-Time) distintos e gerencia tipos primitivos de dados de forma diferente do V8;
- O runtime do Bun é escrito na linguagem de programação Zig, permitindo controle granular sobre alocação de memória na pilha e chamadas diretas de baixo nível do sistema operacional.
A compatibilidade do Bun com ecossistemas legados foi aprimorada, corrigindo bugs do passado com o Prisma Query Engine em arquivos de sistema locais.
9. O papel de io_uring no desempenho do Linux#
Uma das grandes vantagens de desempenho do Bun sob plataformas Linux é o suporte nativo ao io_uring, um subsistema assíncrono moderno do Kernel Linux introduzido a partir da versão 5.1.
- Tradicionalmente, operações assíncronas de arquivos no Linux utilizam chamadas de sistema bloqueantes delegadas a threads de suporte da biblioteca de rede (como na libuv);
- Com o
io_uring, o aplicativo submete requisições de leitura e escrita diretamente a filas de buffers mapeadas em memória compartilhada com o Kernel Linux (Submission Queue e Completion Queue); - Isso elimina o overhead gerado por trocas de contexto (context switching) e chamadas de sistema (
syscalls) repetitivas por conexão, permitindo que um único thread de processo execute milhares de leituras de sockets sem travar a CPU do host.
Vale destacar que versões modernas da libuv do Node.js também suportam a ativação do io_uring de forma opcional sob ambientes Linux compatíveis.
10. A importância da plataforma do sistema operacional#
A execução de benchmarks no sistema operacional Linux Mint (distribuição baseada no ecossistema estável do Ubuntu/Debian) fornece um ambiente altamente consistente para avaliar runtimes. As métricas coletadas em Mint são diretamente equivalentes a servidores de produção rodando Ubuntu LTS. Para garantir a fidelidade técnica, as configurações do kernel (como o número máximo de arquivos abertos de soquete) devem ser otimizadas editando as diretivas do arquivo de configuração do sistema /etc/security/limits.conf:
# Exemplo de enrijecimento de limites no /etc/security/limits.conf
* soft nofile 65536
* hard nofile 65536
11. Metodologia de benchmark de produção#
A metodologia utilizada no Trench Benchmark seguiu parâmetros rigorosos de engenharia de software para garantir a reprodutibilidade dos resultados:
- Hardware Host: Processador AMD Ryzen 7 5800X (8 Cores, 16 Threads), 32 GB RAM DDR4, SSD NVMe Samsung 980 Pro;
- Sistema Operacional: Linux Mint 22 (Kernel LTS 6.8.0);
- Runtimes Testados: Node.js v22.2.0, Bun v1.1.8, Elixir v1.17.0 (OTP 27, Phoenix v1.7.12);
- Ferramenta de Carga:
wrk2executada a partir de uma máquina auxiliar conectada via rede local Gigabit dedicada, evitando que a geração de carga dispute recursos com o servidor; - Cenários do Teste:
- Cenário A: Endpoint REST simples com retorno de objeto serializado em JSON;
- Cenário B: Consulta SQL sequencial (Read) no banco PostgreSQL através de cada ORM/Query wrapper (Prisma no Node/Bun, Ecto no Phoenix).
- Tempo do Teste: 5 minutos de estresse contínuo após período de warm-up (aquecimento) de 30 segundos.
12. Resultados e métricas quantitativas#
As métricas brutas consolidadas durante a bateria de testes de estresse revelaram os seguintes comportamentos sob concorrência de 1.000 clientes virtuais simultâneos:
Tabela de desempenho comparativo#
| Métrica Coletada | Node.js (LTS) + Prisma | Bun.js + Prisma | Elixir / Phoenix + Ecto |
|---|---|---|---|
| Vazão Máxima (Req/s) | 12.450 req/s | 24.120 req/s | 18.980 req/s |
| Latência Média (p50) | 12,4 ms | 4,2 ms | 5,8 ms |
| Latência Cauda (p99) | 88,2 ms | 18,9 ms | 8,9 ms |
| Consumo Médio de RAM | 145 MB | 72 MB | 110 MB |
| Tempo de Cold Start | 120 ms | 15 ms | 210 ms |
O tempo de cold start (tempo gasto entre a execução do binário e a capacidade de responder à primeira requisição HTTP) foi mensurado para avaliar o comportamento em topologias serverless e de escalonamento elástico de contêineres:
Cold Start de Runtimes:
Node.js: ████████████ 120ms
Bun: █ 15ms
Elixir: █████████████████████ 210ms
Análise: O Bun demonstrou a maior vazão pura de requisições simples e o menor tempo de inicialização, beneficiando-se do motor JavaScriptCore. Por outro lado, o Elixir apresentou a latência p99 mais estável e previsível sob estresse extremo, comprovando que o coletor de lixo preemptivo da BEAM mitiga travamentos cíclicos de latência (Stop-The-World garbage collection) comuns no V8 do Node.js.
13. Segurança de runtimes em ambientes Linux#
Ao operar ambientes com Node.js, Bun ou BEAM em produção, o enrijecimento (hardening) do sistema operacional é mandatório para mitigar a superfície de ataques a processos.
Diretrizes de isolamento e privilégios#
- Privilégios Mínimos: Nunca execute serviços de correio ou runtimes de aplicação como usuário
root. Crie um usuário dedicado com privilégios reduzidos no shell:
# Criar usuário sem privilégios administrativos
useradd -M -s /usr/sbin/nologin app_runtime
- Hardening de Diretórios: Garanta que as dependências baixadas na raiz do projeto não pertençam a contas com capacidade de escrita direta pelo processo da aplicação.
- Isolamento de Processos: Utilize namespaces do Kernel Linux (via Docker ou Systemd Sandboxing) combinando perfis restritivos de Seccomp (Secure Computing Mode) para impedir que o Bun (compilado em Zig) faça syscalls de kernel indesejadas que possam comprometer a segurança da infraestrutura.
14. Observabilidade e monitoramento SRE#
Para obter visibilidade sobre o comportamento interno das aplicações em execução contínua, os times SRE devem implementar monitoramento estruturado baseado em OpenTelemetry (OTel).
- Métricas Chave: Rastreie o consumo de memória física RSS (Resident Set Size), quantidade de conexões WebSockets ativas e a latência de loops de eventos;
- Observabilidade no Elixir: Utilize a biblioteca nativa
:telemetrye o painelPhoenix.LiveDashboardpara inspecionar filas de processos pendentes e uso de memória por atores individuais em tempo real; - Logs Estruturados: Configure os runtimes para exportar logs de transações exclusivamente em formato JSON para o log centralizado do Linux (Journald ou Syslog), facilitando buscas automatizadas.
15. Gerenciamento de versões com fnm e asdf#
Em ambientes de desenvolvimento no Linux Mint, a instalação global de pacotes via diretivas sudo npm install -g polui o diretório de pacotes do sistema /usr/lib/ e gera riscos de segurança. Recomendamos gerenciar as ferramentas na pasta de usuário (User-Level).
Para instalar e configurar o fnm (Fast Node Manager), um gerenciador rápido escrito em Rust, adicione os apontamentos de ambiente nos arquivos de perfil do terminal:
# Baixar e instalar o fnm na pasta local de usuário
curl -fsSL https://fnm.vercel.app/install | bash
Após a instalação, configure o PATH adicionando as linhas ao final do seu arquivo de configuração ~/.bashrc ou ~/.zshrc:
# Inicializar fnm no terminal
export PATH="$HOME/.local/share/fnm:$PATH"
eval "`fnm env`"
Como alternativa de governança de múltiplas stacks (gerenciando Node, Elixir, Erlang e Python em um único local), adote o asdf ou gerenciadores dedicados como o nvm ou o volta:
# Apontamento de inicialização local de plugins no asdf
source $HOME/.asdf/asdf.sh
16. Matriz decisória de runtimes#
Para escolher a melhor arquitetura de acordo com o escopo do projeto, utilize os parâmetros definidos na tabela comparativa abaixo:
| Requisito do Projeto | Node.js (LTS) | Bun.js | Elixir / Phoenix |
|---|---|---|---|
| Base de Código Legada | Recomendado | Suporte de Compatibilidade | Rewrite Total Necessário |
| Concorrência de WebSockets | Nível Médio | Alto Desempenho | Ideal (Escala BEAM) |
| Baixo Tempo de Inicialização | Médio | Ideal (15ms) | Alto |
| Programação Funcional | Não nativo | Não nativo | Nativo de Paradigma |
| Ecossistema de Desenvolvedores | Máximo | Alto (JavaScript) | Restrito / Especializado |
17. Checklist SRE para avaliação de mudança de stack#
Antes de iniciar a transição de serviços de Node.js para novas stacks, valide as seguintes etapas:
- [ ] Auditoria de Custo de Migração: Mapear o tempo estimado para reescrever lógica imperativa em funcional.
- [ ] Metodologia de Teste de Carga: Estruturar cenários de teste reais no Linux Mint usando ferramentas como
wrk2. - [ ] Configurações do Kernel Linux: Garantir os limites de arquivos abertos (descriptors) adequados no arquivo
/etc/security/limits.conf. - [ ] Isolamento de Contêineres: Desenhar arquivos Dockerfile não-root para executar os binários da aplicação.
- [ ] Hardening de Chamadas de Sistema: Configurar regras de Seccomp para barrar chamadas não autorizadas ao Kernel.
- [ ] Políticas de Coleta de Logs: Padronizar logs em JSON estruturado para monitoramento contínuo.
- [ ] Configuração do API Gateway: Desenhar as regras de proxy reverso e balanceamento do gateway Nginx.
- [ ] Monitoramento de Memória: Configurar painéis de observabilidade para rastrear o crescimento do cache em memória e conexões WebSocket.
- [ ] Verificação de Dependências: Garantir ambientes locais limpos utilizando FNM ou ASDF sem privilégios de root.
- [ ] Plano de Rollback Incremental: Projetar o plano de rollback imediato no API Gateway caso o novo microsserviço falhe.
18. Matriz de riscos operacionais#
| Evento de Risco | Severidade | Descrição Técnica | Mitigação Técnica Recomendada |
|---|---|---|---|
| Bloqueio do Event Loop | Alta | Operações pesadas síncronas de CPU congelam a thread única do Node.js, bloqueando conexões. | Delegar tarefas pesadas de CPU a processos filhos (Worker Threads) ou microsserviços Elixir dedicados. |
| Incompatibilidade de APIs no Bun | Média | O Bun usa JavaScriptCore; divergências em APIs legadas do V8 podem causar erros inesperados de tempo de execução. | Executar suites completas de testes automatizados de integração antes de deploy em produção. |
| Vazamento de Memória por WebSocket | Alta | Milhares de conexões stateful no LiveView mantêm processos em memória abertos na BEAM, gerando estouros de RAM. | Configurar timeouts agressivos de inatividade para WebSockets e monitorar métricas de RAM da VM. |
| Permissão Insegura no Install | Média | Utilizar sudo npm install ou root global expõe o sistema a códigos maliciosos inseridos em dependências NPM. | Adotar gerenciadores locais (FNM ou Volta) instalados na pasta de usuário sem privilégios elevados. |
| Latência Adicional no Strangler Fig | Baixa | O redirecionamento de conexões pelo API Gateway entre Node e Elixir introduz pequenos milissegundos adicionais. | Otimizar conexões de rede locais e configurar keep-alive persistente no gateway Nginx. |
Este artigo foi útil?
Deixe uma reação rápida para apoiar o conteúdo:
Este post está licenciado sob CC BY-NC.



Comentários
Participe da discussão abaixo.
0 comentários