SSD Nodes Learn 🎉 VPS desde $5.50/mês
Guias Matt ConnorPor Matt Connor

NATS vs RabbitMQ vs Kafka: qual usar num VPS?

Compare NATS, RabbitMQ, Kafka e Postgres num único VPS: garantias de entrega, custo de memória e disco, reinício e como verificar o backlog.

A resposta curta para um único servidor

Uma fila de mensagens num único VPS é uma decisão sobre garantias de entrega, não sobre velocidade. Num único servidor, o broker raramente é o gargalo, porque o código da aplicação, a base de dados e o único disco chegam primeiro ao limite. Escolha a ferramenta cujo comportamento em caso de falha aceita gerir e, em seguida, meça o servidor que realmente tem.

Quatro opções, pela ordem que a maioria dos leitores deve considerar.

  • Use a base de dados que já executa. O Postgres com SELECT ... FOR UPDATE SKIP LOCKED é uma fila de tarefas funcional e não adiciona nenhum processo novo para monitorizar.
  • Use RabbitMQ quando cada mensagem for uma unidade de trabalho que tem de ser confirmada, repetida um número limitado de vezes e depois colocada num local que uma pessoa possa consultar.
  • Use NATS quando as mensagens forem eventos aos quais várias partes do sistema reagem. Ative o JetStream para os eventos que têm de sobreviver a um reinício.
  • Use Kafka quando uma ferramenta downstream comunicar apenas através do protocolo Kafka. Num único servidor, essa é praticamente a única razão que resta.

O restante deste guia explica os motivos: quanto cada opção custa em memória e disco num VPS pequeno, o que faz quando o servidor reinicia e o comando exato que mostra uma acumulação de mensagens antes de os utilizadores a sentirem.

O que uma garantia de entrega realmente significa

No máximo uma vez significa que o broker entrega a mensagem e deixa de a acompanhar. Se nenhum consumidor estiver ligado ou se um consumidor terminar a execução a meio do processamento, a mensagem perde-se e nada comunica o problema.

Pelo menos uma vez significa que o consumidor envia uma confirmação (um ack) depois de o processamento terminar com sucesso. Até esse ack chegar, o broker mantém a mensagem e volta a entregá-la. A reentrega é a razão pela qual os seus handlers têm de ser idempotentes: processar a mesma mensagem duas vezes não pode cobrar o cartão duas vezes. A entrega exatamente uma vez, de ponta a ponta, não é algo que um broker forneça. Ela resulta de uma chave única na sua própria base de dados.

Replay é uma propriedade diferente. Uma fila descarta a mensagem depois de receber a confirmação. Um log mantém a mensagem durante uma janela de retenção, para que um novo consumidor possa começar no início e ler todo o histórico. Kafka e NATS JetStream são logs. RabbitMQ é uma fila. Essa diferença influencia mais arquiteturas do que o débito. A letra morta é o que acontece a uma mensagem que falha repetidamente. Sem esse mecanismo, uma mensagem problemática entra num ciclo infinito, que parece indicar um worker ocupado em vez de um worker avariado.

Comece com o Postgres e faça o broker provar o seu valor

A maioria das cargas de trabalho de uma única aplicação corresponde a alguns milhares de tarefas em segundo plano por dia. Isso cabe numa tabela.

CREATE TABLE job (
  id        bigserial   PRIMARY KEY,
  payload   jsonb       NOT NULL,
  run_after timestamptz NOT NULL DEFAULT now(),
  attempts  int         NOT NULL DEFAULT 0
);
CREATE INDEX job_ready_idx ON job (run_after, id);

Um worker reivindica uma tarefa dentro de uma transação.

BEGIN;
SELECT id, payload
  FROM job
 WHERE run_after <= now()
 ORDER BY id
   FOR UPDATE SKIP LOCKED
 LIMIT 1;
-- run the work, then remove the row
DELETE FROM job WHERE id = $1;
COMMIT;

FOR UPDATE SKIP LOCKED é o mecanismo completo. Bloqueia a linha devolvida e ignora qualquer linha que outra transação já tenha bloqueado, por isso dois workers nunca reivindicam a mesma tarefa. Se um worker falhar, o Postgres aborta a transação, liberta o bloqueio e a linha fica visível para o worker seguinte. Obtém entrega pelo menos uma vez, novas tentativas incrementando attempts e uma tabela de dead letter, tudo com a durabilidade que já paga. O backlog é uma consulta: SELECT count(*) FROM job WHERE run_after <= now();

Onde deixa de funcionar. Cada reivindicação e eliminação é uma escrita, por isso uma taxa elevada de tarefas deixa versões de linhas obsoletas, e uma tabela de fila é o caso clássico em que o bloat ultrapassa a capacidade do autovacuum. Tarefas longas pioram o problema, porque uma transação mantida aberta durante toda a execução também atrasa o horizonte do vacuum para toda a base de dados. O polling acrescenta latência, e LISTEN com NOTIFY elimina o polling, mas não as escritas. Quando a tabela de tarefas é a tabela mais ocupada que tem, ou quando um segundo serviço precisa dos mesmos eventos, retire o trabalho da base de dados. Essa escolha depende da forma como a própria base de dados é implementada, por isso defina se a base de dados corre no Docker ou no host antes de adicionar um broker ao lado dela.

Redis é a outra opção que pode já estar a executar. Redis Streams disponibiliza grupos de consumidores com XADD e XREADGROUP, uma lista de pendentes por grupo e XAUTOCLAIM para recuperar o trabalho de um consumidor que terminou inesperadamente. É pequeno e rápido. O ponto importante num único servidor: com a definição comum appendfsync everysec, uma perda de energia pode fazer perder cerca de um segundo de escritas. Isso é aceitável para invalidação de cache e inadequado para pagamentos. Se a sua aplicação for um único processo baseado em SQLite em produção num VPS, o mesmo padrão de reivindicação e eliminação funciona, embora o SQLite não tenha um equivalente de SKIP LOCKED e todos os workers sejam serializados no único bloqueio de escrita.

Roteamento de subjects no NATS core sem retenção

docker run -d --name nats \
  -p 4222:4222 -p 127.0.0.1:8222:8222 \
  nats:2.14 -m 8222

Em agosto de 2026, a linha atual do servidor é a 2.14. -m 8222 ativa a porta de monitorização HTTP, que está desativada por predefinição e não tem autenticação. Por isso, associe-a a localhost, conforme indicado acima.

O Core NATS oferece entrega no máximo uma vez e não armazena dados. Um publisher envia uma mensagem para um subject, como orders.created, e cada subscriber cujo filtro corresponda recebe uma cópia. Se não houver nenhum subscriber, a mensagem é descartada e o publisher não recebe um erro, porque o trabalho do publisher terminou quando o servidor aceitou os bytes. Um queue group, composto por vários subscribers que partilham o mesmo nome de grupo, faz com que o servidor escolha um membro por mensagem. Assim, a carga de trabalho é distribuída sem armazenar uma fila.

O consumo de recursos consiste no estado das subscrições e num buffer de escrita para cada ligação. Por isso, depende do número de ligações e não do volume de mensagens. Nada se acumula em disco. O comportamento após um reinício decorre disso: as mensagens em trânsito são perdidas, os clientes voltam a ligar-se automaticamente e não há nenhuma etapa de recuperação pela qual seja necessário esperar.

Não existe backlog para monitorizar. Em vez disso, monitorize as perdas. Quando um subscriber lê o seu socket mais lentamente do que o servidor escreve nele, o buffer do servidor para esse cliente fica cheio. Se o cliente não acompanhar o ritmo até ao fim do prazo de escrita, o servidor fecha toda a ligação e incrementa um contador.

curl -s http://localhost:8222/varz | jq '.slow_consumers, .connections, .in_msgs, .out_msgs'

Um valor de slow_consumers que continua a aumentar significa que as mensagens estão a ser descartadas. Por isso, crie um alerta para esse valor em vez de o consultar apenas uma vez. O Core NATS é adequado para mensagens cujo valor expira rapidamente: uma métrica, uma atualização de presença ou uma invalidação de cache que será substituída pelo evento seguinte.

NATS JetStream: streams duráveis e replay no mesmo processo

O JetStream não é um segundo produto. É um subsistema no mesmo binário, ativado por uma única flag.

docker run -d --name nats \
  -p 4222:4222 -p 127.0.0.1:8222:8222 \
  -v nats-data:/data \
  nats:2.14 -js -sd /data -m 8222

-sd /data define o diretório de armazenamento. Se o omitir, o JetStream armazena os dados em /tmp, com uma durabilidade exatamente igual à que o nome sugere. Crie um stream com a CLI, incluída na imagem nats-box.

docker run --rm -it --network host natsio/nats-box:latest \
  nats stream add ORDERS \
    --subjects 'orders.>' \
    --storage file \
    --retention limits \
    --max-age 72h \
    --max-bytes=1073741824 \
    --discard old \
    --defaults

Cada limite é importante num servidor pequeno. --storage file determina o que sobrevive a uma falha, porque um stream em memória não sobrevive. --max-bytes=1073741824 limita o stream a 1 GiB, indicado como quantidade de bytes, e --discard old remove as mensagens mais antigas quando o limite é atingido, em vez de recusar novas escritas. Se não definir o limite, um publisher descontrolado enche o disco. Nesse momento, a base de dados também para, porque partilha esse disco.

Um consumer durável mantém a sua posição no stream e conserva-a após um restart. Defina --max-deliver no consumer para que uma mensagem que falha sempre deixe de ser redistribuída indefinidamente. Quando uma mensagem esgota o número de entregas, o JetStream publica um aviso em $JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.>. Subscrever esse subject é a forma de criar o percurso de dead letter que o RabbitMQ fornece como funcionalidade. Esse trabalho tem de ser implementado por si.

Para ver um backlog, execute nats stream report para obter as contagens de mensagens armazenadas e nats consumer report ORDERS para obter os acknowledgements pendentes e as mensagens não processadas por consumer. O número de mensagens não processadas é aquele que deve gerar um alerta. O espaço ocupado em disco pode ser consultado com du -sh no diretório de armazenamento. Esse valor aumenta até que um limite de retenção o reduza.

RabbitMQ: confirme cada mensagem e coloque as falhas em espera

docker run -d --name rabbitmq \
  -p 5672:5672 -p 127.0.0.1:15672:15672 \
  -v rabbitmq-data:/var/lib/rabbitmq \
  rabbitmq:4-management

Em agosto de 2026, a série atual é a 4.3. A porta 5672 usa AMQP (advanced message queuing protocol) e a porta 15672 é a interface de gestão. Mantenha a interface em localhost e aceda a ela através de um túnel SSH.

Declare as filas com o argumento x-queue-type definido como quorum; o valor predefinido continua a ser classic. As quorum queues são sempre duráveis e escrevem os dados no disco antes de fazer qualquer outra coisa. Assim, num único nó, obtém um comportamento claro em vez de uma combinação de opções duráveis e transitórias. Defina o destino de dead letter através de uma policy.

docker exec rabbitmq rabbitmqctl set_policy DLX ".*" \
  '{"dead-letter-exchange":"my-dlx", "dead-letter-routing-key":"my-routing-key"}' \
  --apply-to queues --priority 7

Uma mensagem é enviada para dead letter por quatro motivos: um consumer rejeita-a com basic.reject ou basic.nack e requeue definido como false; o TTL (time to live) da mensagem expira; a fila ultrapassa um limite de comprimento; ou a mensagem excede o limite de entregas de uma quorum queue. A partir do RabbitMQ 4.0, esse limite é 20 por predefinição. Assim, um handler que lança uma exceção e faz nack tenta entregar a mensagem vinte vezes e depois envia-a para o dead letter exchange, em vez de entrar num ciclo.

A memória é uma fonte comum de problemas do RabbitMQ numa VPS pequena. O high watermark predefinido é 0.6 da RAM disponível. Quando o nó ultrapassa esse valor, o RabbitMQ bloqueia todas as ligações que publicam mensagens. A aplicação não recebe um erro. Recebe uma operação de publicação que nunca termina, o que parece um bloqueio no próprio código. O log de arranque apresenta o valor calculado pelo nó:

Memory high watermark set to 1024 MiB (1073741824 bytes) of 8192 MiB (8589934592 bytes) total

O alarme de disco bloqueia os publishers da mesma forma quando o espaço livre desce abaixo de 50 MB por predefinição. As quorum queues acrescentam os seus próprios requisitos: a documentação calcula pelo menos 32 bytes de metadados em memória por mensagem, cerca de 1 MB por 30,000 mensagens, e recomenda pelo menos três vezes o tamanho efetivo do write-ahead log em RAM. O limite do WAL é 512 MiB por predefinição. Só essa recomendação exige 1.5 GB. Num servidor com 2 GB, reduza-o em rabbitmq.conf em vez de assumir que o valor predefinido é adequado.

raft.wal_max_size_bytes = 64000000
vm_memory_high_watermark.relative = 0.5

O backlog tem dois números, e o par indica qual é a falha.

docker exec rabbitmq rabbitmqctl list_queues name messages messages_ready messages_unacknowledged

messages_ready está à espera de um consumer. messages_unacknowledged foi entregue e nunca recebeu ack. Um número crescente de mensagens não confirmadas junto de um número estável de mensagens prontas indica que os seus workers receberam os trabalhos e deixaram de os concluir. Esse é um problema diferente de uma fila que está simplesmente atrasada.

Kafka em uma única máquina e quando deixa de fazer sentido

KAFKA_CLUSTER_ID="$(bin/kafka-storage.sh random-uuid)"
bin/kafka-storage.sh format --standalone -t $KAFKA_CLUSTER_ID -c config/server.properties
bin/kafka-server-start.sh config/server.properties

Esse é o guia de início rápido do Kafka 4.3.1, atualizado em agosto de 2026, executado no modo KRaft (Kafka Raft, o controlador integrado que substituiu o ZooKeeper no Kafka 4.0). O equivalente em contêiner é apache/kafka:4.3.1.

O script de inicialização define export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G" quando você ainda não o definiu. Assim, o broker reserva um heap Java de 1 GB antes de armazenar uma única mensagem e espera que exista RAM livre adicional para o page cache que utiliza. Em uma VPS com 2 GB, a sua aplicação passa a disputar com a JVM a memória restante.

A retenção é a próxima surpresa. log.retention.hours tem o valor padrão 168, que corresponde a sete dias, e log.retention.bytes tem o valor padrão -1, que significa que não existe limite de tamanho. O Kafka mantém as mensagens durante toda a janela, independentemente de todos os consumidores já as terem lido. Essa é a funcionalidade que você procurava. Em um disco pequeno, ela também é o modo de falha. Portanto, defina um limite em bytes por tópico antes de descobrir isso.

Agora, a parte honesta. Um único broker significa fator de replicação 1. Portanto, acks=all resulta em um fsync em um único disco. Você obtém a durabilidade de uma única máquina, com o custo operacional de um broker JVM e de um controlador. As partições fornecem paralelismo entre brokers que você não possui. A replicação, o reconhecimento de rack e os restantes recursos de uma frota ficam inativos. O JetStream oferece a mesma reprodução durável na mesma máquina, com uma fração da memória. Ainda existem duas razões para usar Kafka neste caso: uma ferramenta downstream fala apenas o protocolo Kafka (captura de alterações com Debezium ou um carregador de dados analíticos), ou você está reproduzindo uma topologia de produção em escala reduzida. Planejar crescer até um cluster significa planejar comprar mais máquinas. Até lá, o compromisso é o mesmo de executar o k3s em um único nó, em que você paga a complexidade de um cluster pela fiabilidade de um único nó.

O backlog no Kafka é o consumer lag.

bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group my-group

Leia a coluna LAG. Ela corresponde a LOG-END-OFFSET menos CURRENT-OFFSET para cada partição. Um lag crescente em uma partição enquanto as restantes permanecem estáveis indica uma chave desequilibrada, porque todas as mensagens com a mesma chave vão para a mesma partição e um único consumidor as processa sozinho.

O que acontece quando o servidor reinicia

O Core NATS perde tudo o que estava em trânsito e volta imediatamente, porque não há nada para recuperar. O JetStream recarrega os streams e as posições dos consumidores a partir do diretório de armazenamento, para que os consumidores retomem no offset em que estavam. O RabbitMQ recupera as filas quorum a partir do disco, enquanto as filas transient clássicas e todas as mensagens publicadas sem o modo de entrega persistent desaparecem. O Kafka reproduz os segmentos do log no arranque. Depois de um encerramento não limpo, essa verificação de recuperação pode demorar minutos num disco pequeno antes de o broker aceitar uma ligação.

Vale a pena definir duas coisas uma vez. Atribua ao container uma política de reinício (restart: unless-stopped) ou ative a unidade systemd, para que o broker volte a arrancar depois de um reboot causado por uma atualização do kernel sem intervenção manual. Depois, trate da ordem de arranque. Um broker que fica pronto vinte segundos depois da aplicação recusa as primeiras ligações. Algumas bibliotecas de cliente terminam o processo em vez de repetir a tentativa. Faça a aplicação depender do broker com healthchecks do Compose que mantêm um serviço dependente parado até o broker estar pronto.

Custo no seu próprio VPS, medido em vez de citado

Os valores de throughput publicados são medidos em hardware que você não tem, normalmente um servidor com vários núcleos e NVMe local. Trate-os como um limite superior e meça o seu servidor.

docker stats --no-stream
free -m
sudo du -sh /var/lib/docker/volumes/*/_data

Execute esses testes com o broker ocioso e depois novamente com o seu tráfego real. A diferença entre os dois valores determina se o broker pode funcionar junto da sua aplicação. Para obter um limite inferior aproximado de throughput, use o gerador de carga do próprio projeto, em vez de um artigo de blog: nats bench pub test --msgs 100000 --clients 2 para NATS, bin/kafka-producer-perf-test.sh para Kafka e PerfTest para RabbitMQ. Executar o gerador no mesmo VPS mede o broker e o gerador em conjunto. Isso é aceitável, desde que essa condição seja indicada ao reportar o valor.

Existe um limite comum a todos eles. Todas as opções de persistência desta lista dependem de fsync. Por isso, num VPS com armazenamento ligado pela rede, o disco define o limite, e trocar de broker não o altera.

Três cargas de trabalho e a fila de mensagens adequada para cada uma

  1. Tarefas em segundo plano para uma aplicação Web, como enviar e-mails, redimensionar imagens ou entregar webhooks. Comece com Postgres e SKIP LOCKED. Mude para RabbitMQ com filas quorum quando precisar de confirmações por mensagem, de um limite de entregas e de uma dead letter queue que possa inspecionar sem implementar essa lógica, ou quando a tabela de tarefas se tornar a tabela mais ocupada da base de dados.
  2. Eventos aos quais vários serviços internos reagem, quando uma mensagem perdida é rapidamente substituída por uma mais recente. Use o Core NATS, com subjects como esquema de encaminhamento e queue groups quando precisar de partilha de trabalho. Adicione um stream JetStream para o conjunto restrito de subjects que tem de sobreviver a um reinício e mantenha o restante em memória.
  3. Um registo de eventos que os consumidores leem desde o início, para uma trilha de auditoria, reconstruir um read model ou alimentar análises posteriormente. Use JetStream com armazenamento em ficheiros e um limite explícito em bytes. Escolha Kafka apenas quando uma ferramenta a jusante exigir o protocolo Kafka e aceite a heap da JVM como custo dessa compatibilidade.

Numa única máquina, o custo de uma escolha errada não é o throughput. É a recuperação às três da manhã, quando precisa de saber se as mensagens ainda existem. Escolha com base nisso.

FAQ

Posso executar Kafka numa VPS com 2 GB?

Arranca, mas os recursos ficam apertados. bin/kafka-server-start.sh define KAFKA_HEAP_OPTS="-Xmx1G -Xms1G" quando não o substituiu, por isso a JVM reserva 1 GB antes de armazenar qualquer mensagem. O Kafka também depende de memória livre adicional para a cache de páginas. Se adicionar a aplicação e a base de dados no mesmo servidor, começa a usar swap. Também fica com um fator de replicação de 1. Isto significa que acks=all corresponde a um único fsync num único disco. Assim, assume o custo operacional do Kafka sem obter o seu modelo de durabilidade. O NATS JetStream permite reproduzir mensagens de forma durável no mesmo hardware, usando muito menos memória.

Preciso de uma fila de mensagens se já executar o Postgres?

Muitas vezes, não. Uma tabela job lida com SELECT ... FOR UPDATE SKIP LOCKED dentro de uma transação fornece entrega pelo menos uma vez, workers concorrentes seguros, novas tentativas e uma tabela de mensagens não entregues. Não requer outro serviço para monitorizar e usa cópias de segurança que já faz. Os sinais para mudar de solução são específicos: a tabela da fila torna-se a carga de escrita mais pesada e o autovacuum fica atrasado; jobs de longa duração mantêm transações abertas e bloqueiam o vacuum de toda a base de dados; ou um segundo serviço precisa de consumir os mesmos eventos de forma independente.

Devo usar NATS JetStream ou RabbitMQ para jobs em segundo plano?

Use RabbitMQ se precisar de confirmação por mensagem, de um limite de entrega e de encaminhamento para mensagens não entregues como comportamentos integrados. As filas quorum são sempre duráveis. O limite de entrega predefinido é 20 a partir do RabbitMQ 4.0. Uma política envia as mensagens cujo limite foi excedido para um dead letter exchange, que pode esvaziar e inspecionar. Use JetStream se os mesmos eventos também precisarem de ser reproduzidos mais tarde por outros consumidores. Um stream mantém as mensagens depois da confirmação, enquanto uma fila não mantém. Com JetStream, define --max-deliver e constrói manualmente o fluxo de mensagens não entregues a partir do aviso $JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.>.

Como posso saber até que ponto os meus consumidores estão atrasados?

Cada broker tem um comando. No RabbitMQ, rabbitmqctl list_queues name messages messages_ready messages_unacknowledged separa o trabalho à espera de um consumidor do trabalho entregue mas nunca confirmado. No JetStream, nats consumer report <stream> mostra as mensagens não processadas e as confirmações pendentes por consumidor. No Kafka, kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group <group> apresenta uma coluna LAG por partição. O Core NATS não tem backlog para consultar, porque não armazena nada. Em vez disso, monitorize o contador slow_consumers em http://localhost:8222/varz. Esse contador indica as ligações que o servidor fechou por ficarem atrasadas, o que significa perda de mensagens.

#nats#rabbitmq#kafka#message-queue#architecture