SSD Nodes Learn 🎉 VPS desde $5.50/mes
Guías Matt ConnorPor Matt Connor

NATS vs RabbitMQ vs Kafka en un solo VPS

Compare NATS, RabbitMQ, Kafka y Postgres en un solo VPS: garantías de entrega, memoria, disco, reinicios y comprobación de acumulaciones.

La respuesta breve para un solo servidor

Una cola de mensajes en un único VPS es una decisión sobre las garantías de entrega, no sobre la velocidad. En un solo equipo, el broker rara vez es el cuello de botella, porque el código de la aplicación, la base de datos y el único disco suelen llegar antes a sus límites. Elija la herramienta cuyo comportamiento ante fallos esté dispuesto a aceptar y mida el equipo que realmente tiene.

Estas son cuatro opciones, en el orden que la mayoría de los lectores debería considerar.

  • Use la base de datos que ya ejecuta. Postgres con SELECT ... FOR UPDATE SKIP LOCKED es una cola de trabajos funcional y no añade ningún proceso nuevo que deba monitorizar.
  • Use RabbitMQ cuando cada mensaje sea una unidad de trabajo que deba confirmarse, reintentarse un número limitado de veces y después almacenarse en un lugar que una persona pueda revisar.
  • Use NATS cuando los mensajes sean eventos a los que reaccionen varias partes del sistema. Active JetStream para los eventos que deban sobrevivir a un reinicio.
  • Use Kafka cuando una herramienta posterior sólo hable el protocolo Kafka. En un solo servidor, esa es casi la única razón que queda.

El resto de esta guía explica los motivos: cuánto cuesta cada opción en memoria y disco en un VPS pequeño, qué ocurre cuando se reinicia el equipo y cuál es el comando exacto que muestra una acumulación de mensajes antes de que los usuarios la perciban.

Qué significa realmente una garantía de entrega

Como máximo una vez significa que el broker entrega el mensaje y lo descarta. Si no hay ningún consumidor conectado o un consumidor falla a mitad del trabajo, el mensaje se pierde y no se informa de ello.

Al menos una vez significa que el consumidor envía una confirmación (un ack) después de completar correctamente el trabajo. Hasta que llega ese ack, el broker conserva el mensaje y volverá a entregarlo. La redeliveración obliga a que los controladores sean idempotentes: procesar dos veces el mismo mensaje no debe cobrar dos veces la tarjeta. La entrega exactamente una vez, de extremo a extremo, no la proporciona un broker. Se obtiene mediante una clave única en la base de datos propia.

Reproducir mensajes es una propiedad independiente. Una cola descarta un mensaje cuando se confirma. Un log lo conserva durante una ventana de retención, de modo que un consumidor nuevo puede comenzar desde el principio y leer todo el historial. Kafka y NATS JetStream son logs. RabbitMQ es una cola. Esta diferencia determina más arquitecturas que el rendimiento.

Enviar a una cola de mensajes fallidos es lo que ocurre con un mensaje que sigue fallando. Sin esta función, un mensaje problemático se procesa en un bucle infinito, que parece un worker ocupado en lugar de uno averiado.

Empiece con Postgres y haga que el broker demuestre su utilidad

La mayoría de las cargas de trabajo de una sola aplicación generan unos pocos miles de trabajos en segundo plano al día. Eso cabe en una tabla.

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);

Un worker reclama un trabajo dentro de una transacción.

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 es todo el mecanismo. Bloquea la fila que devuelve e ignora cualquier fila que otra transacción ya haya bloqueado, de modo que dos workers nunca reclaman el mismo trabajo. Si un worker falla, Postgres aborta su transacción, libera el bloqueo y la fila vuelve a estar disponible para el siguiente worker. Obtiene entrega al menos una vez, reintentos incrementando attempts y una tabla de mensajes fallidos, todo ello con la durabilidad que ya está pagando. El backlog se consulta con una sola consulta: SELECT count(*) FROM job WHERE run_after <= now();

Cuándo deja de funcionar. Cada reclamación y cada eliminación es una escritura, por lo que una tasa alta de trabajos deja versiones de filas obsoletas, y una tabla de cola es el caso clásico en que la fragmentación supera la capacidad de autovacuum. Los trabajos largos lo empeoran, porque una transacción abierta durante toda la ejecución también retrasa el horizonte de vacuum de toda la base de datos. El polling añade latencia, y LISTEN con NOTIFY elimina el polling, pero no las escrituras. Cuando la tabla de trabajos es la tabla más activa que tiene, o cuando un segundo servicio necesita los mismos eventos, saque el trabajo de la base de datos. Esta decisión depende de cómo se despliega la propia base de datos, así que determine si la base de datos se ejecuta en Docker o en el host antes de añadir un broker junto a ella.

Redis es la otra opción que quizá ya ejecute. Redis Streams proporciona grupos de consumidores con XADD y XREADGROUP, una lista de pendientes por grupo y XAUTOCLAIM para recuperar el trabajo de un consumidor que ha fallado. Es pequeño y rápido. La limitación importante en un solo equipo: con la configuración habitual de appendfsync everysec, un corte de alimentación puede hacer que se pierda aproximadamente un segundo de escrituras. Eso es aceptable para la invalidación de caché y no lo es para los pagos. Si su aplicación es un único proceso basado en SQLite en producción en un VPS, funciona el mismo patrón de reclamar y eliminar, aunque SQLite no tiene un equivalente de SKIP LOCKED y todos los workers se serializan en el único bloqueo de escritura.

NATS principal: enrutamiento por subject sin almacenamiento

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

En agosto de 2026, la línea actual del servidor es 2.14. -m 8222 habilita el puerto de monitorización HTTP, que está desactivado de forma predeterminada y no tiene autenticación. Por tanto, debe vincularlo a localhost como se indicó arriba.

Core NATS ofrece como máximo una entrega y no almacena nada. Un publisher publica en un subject como orders.created, y cada subscriber cuyo filtro coincide recibe una copia. Si no hay ningún subscriber suscrito, el mensaje se descarta y el publisher no recibe ningún error, porque su trabajo terminó cuando el servidor aceptó los bytes. Un queue group (varios subscribers que comparten un nombre de grupo) hace que el servidor elija un miembro por mensaje. Esto distribuye el trabajo sin almacenar una cola.

El consumo de recursos consiste en el estado de las suscripciones y un búfer de escritura por conexión. Por tanto, depende del número de conexiones, no del volumen de mensajes, y no se acumula nada en disco. El comportamiento tras un reinicio se deduce de esto: los mensajes en curso se pierden, los clientes se vuelven a conectar por sí solos y no hay ningún paso de recuperación que deba esperar.

No hay un backlog que monitorizar, así que debe monitorizar las pérdidas. Cuando un subscriber lee su socket más despacio de lo que el servidor escribe en él, se llena el búfer del servidor para ese cliente. Si el cliente no se ha puesto al día cuando vence el plazo de escritura, el servidor cierra toda la conexión e incrementa un contador.

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

Un valor de slow_consumers que sigue aumentando indica que se están descartando mensajes. Por tanto, configure una alerta para ese valor en lugar de consultarlo una sola vez. Core NATS es adecuado para mensajes cuyo valor caduca rápidamente: una métrica, una actualización de presencia o una invalidación de caché que el siguiente evento sustituirá de todos modos.

NATS JetStream: streams persistentes y reproducción en el mismo proceso

JetStream no es un segundo producto. Es un subsistema del mismo binario que se habilita con una sola opción.

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 establece el directorio de almacenamiento. Si se omite, JetStream almacena los datos en /tmp, con una persistencia tan limitada como indica su naturaleza. Cree un stream con la CLI, incluida en la imagen 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 límite tiene una función en un servidor pequeño. --storage file determina qué datos sobreviven a un fallo, porque un stream en memoria no lo hace. --max-bytes=1073741824 limita el stream a 1 GiB escrito como cantidad de bytes, y --discard old descarta los mensajes más antiguos cuando se alcanza el límite en lugar de rechazar nuevas escrituras. Si no establece el límite, un publisher descontrolado llena el disco. En ese momento la base de datos también se detiene porque ambos comparten ese disco.

Un consumidor persistente conserva su posición en el stream y la mantiene tras un reinicio. Establezca --max-deliver en el consumidor para que un mensaje que siempre falla deje de redistribuirse indefinidamente. Cuando un mensaje agota el número de entregas, JetStream publica un aviso en $JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.>. Suscribirse a ese subject permite crear la ruta de mensajes no entregados que RabbitMQ proporciona como funcionalidad. Esa lógica debe implementarla usted.

Para ver los mensajes pendientes, ejecute nats stream report para obtener el recuento de mensajes almacenados y nats consumer report ORDERS para consultar las confirmaciones pendientes y los mensajes sin procesar de cada consumidor. El número de mensajes sin procesar es el que debe activar una alerta. El uso de disco se puede consultar con du -sh sobre el directorio de almacenamiento y aumenta hasta que un límite de retención lo reduce.

RabbitMQ: confirme cada mensaje y aísle los fallos

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

En agosto de 2026, la serie actual es la 4.3. El puerto 5672 usa AMQP (advanced message queuing protocol) y el 15672 es la interfaz de administración. Mantenga la interfaz en localhost y acceda a ella mediante un túnel SSH.

Declare las colas con el conjunto de argumentos x-queue-type establecido en quorum; el valor predeterminado sigue siendo classic. Las colas quorum siempre son duraderas y escriben los datos en disco antes de hacer cualquier otra cosa. Así, en un solo nodo se obtiene un comportamiento claro, en lugar de una combinación de opciones duraderas y transitorias. Establezca el destino de mensajes rechazados mediante una 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

Un mensaje se envía a la dead letter exchange por cuatro motivos: un consumidor lo rechaza con basic.reject o basic.nack y requeue está establecido en false; expira su TTL (time to live) por mensaje; la cola supera un límite de longitud; o supera el límite de entregas de una cola quorum. Desde RabbitMQ 4.0, ese límite es 20 de forma predeterminada. Por tanto, un handler que lanza una excepción y hace nack reintenta veinte veces y después entrega el mensaje a la dead letter exchange, en lugar de entrar en un bucle.

La memoria es donde RabbitMQ suele causar problemas en un VPS pequeño. El high watermark predeterminado es 0.6 de la RAM disponible. Cuando el nodo lo supera, RabbitMQ bloquea todas las conexiones que publican. La aplicación no recibe un error. Recibe una operación de publicación que nunca termina, lo que en su propio código parece un bloqueo. El log de arranque muestra el valor que calculó el nodo:

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

La alarma de disco bloquea los publicadores de la misma forma cuando el espacio libre cae por debajo de 50 MB de forma predeterminada. Las colas quorum añaden sus propios requisitos: la documentación calcula al menos 32 bytes de metadatos en memoria por mensaje, aproximadamente 1 MB por cada 30,000 mensajes, y recomienda disponer en RAM de al menos tres veces el tamaño efectivo del write-ahead log. El límite del WAL es 512 MiB de forma predeterminada, por lo que esa recomendación por sí sola requiere 1.5 GB. En un servidor de 2 GB, redúzcalo en rabbitmq.conf en lugar de confiar en que el valor predeterminado quepa.

raft.wal_max_size_bytes = 64000000
vm_memory_high_watermark.relative = 0.5

El backlog se expresa con dos números. El par indica qué tipo de fallo se ha producido.

docker exec rabbitmq rabbitmqctl list_queues name messages messages_ready messages_unacknowledged

messages_ready está esperando a un consumidor. messages_unacknowledged se entregó y nunca recibió un ack. Un recuento creciente de mensajes sin confirmar junto a un recuento estable de mensajes listos indica que los workers aceptaron los trabajos y dejaron de completarlos. Es un fallo distinto del de una cola que simplemente está atrasada.

Kafka en un solo equipo y cuándo deja de tener 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

Ese es el inicio rápido de Kafka 4.3.1, vigente en agosto de 2026, ejecutado en modo KRaft (Kafka Raft, el controlador integrado que sustituyó a ZooKeeper en Kafka 4.0). El equivalente para contenedores es apache/kafka:4.3.1.

El script de inicio establece export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G" si no lo ha definido usted, por lo que el broker reserva un heap de Java de 1 GB antes de almacenar un solo mensaje y espera disponer de RAM libre adicional para la caché de páginas desde la que lee. En una VPS de 2 GB, su aplicación compite con la JVM por la memoria restante.

La retención es la siguiente sorpresa. log.retention.hours tiene el valor predeterminado 168, es decir, siete días, y log.retention.bytes tiene el valor predeterminado -1, lo que significa que no existe ningún límite de tamaño. Kafka conserva los mensajes durante todo el intervalo, independientemente de que todos los consumidores los hayan leído. Esa es la función que necesita, pero en un disco pequeño también es el modo de fallo. Por eso, establezca un límite en bytes por topic antes de comprobarlo por las malas.

Ahora viene la parte realista. Un único broker implica un factor de replicación de 1, por lo que acks=all se reduce a un fsync en un solo disco. Obtiene la durabilidad de un único equipo, con el coste operativo de un broker JVM más un controlador. Las particiones proporcionan paralelismo entre brokers que usted no tiene. La replicación, la afinidad por rack y el resto de las funciones para flotas permanecen inactivas. JetStream proporciona la misma reproducción persistente en el mismo equipo con una fracción de la memoria. Aquí todavía hay dos motivos para usar Kafka: una herramienta posterior sólo admite el protocolo Kafka (captura de cambios de datos con Debezium o un cargador de analítica), o está reproduciendo una topología de producción a menor escala. Planificar el crecimiento hacia un clúster implica comprar más equipos y, hasta entonces, el intercambio es el mismo que ejecutar k3s en un solo nodo, donde se paga la complejidad de un clúster para obtener la fiabilidad de un solo nodo.

En Kafka, el backlog se denomina consumer lag.

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

Lea la columna LAG, que es LOG-END-OFFSET menos CURRENT-OFFSET para cada partición. Un lag que aumenta en una partición mientras las demás permanecen estables indica una distribución desigual de las claves, porque todos los mensajes con la misma clave llegan a la misma partición y un solo consumidor los gestiona.

Qué ocurre cuando se reinicia el servidor

Core NATS pierde todo lo que estaba en tránsito y vuelve a estar disponible de inmediato, porque no hay nada que recuperar. JetStream vuelve a cargar los streams y las posiciones de los consumers desde el directorio de almacenamiento, por lo que los consumers reanudan desde el offset que tenían. RabbitMQ recupera las colas quorum desde el disco, mientras que las colas transient clásicas y cualquier mensaje publicado sin el modo de entrega persistent se pierden. Kafka vuelve a procesar sus segmentos de log al iniciar. Después de un apagado no limpio, este análisis de recuperación puede tardar varios minutos en un disco pequeño antes de que el broker acepte conexiones.

Conviene establecer dos aspectos una sola vez. Asigne al container una política de reinicio (restart: unless-stopped) o habilite la unidad de systemd, para que el broker vuelva a iniciarse después de un reinicio del kernel sin intervención manual. Después, gestione el orden de inicio. Un broker que está ready veinte segundos después de la aplicación rechazará las primeras conexiones, y algunas client libraries terminan el proceso en lugar de reintentar. Haga que la aplicación dependa del broker mediante healthchecks de Compose que mantienen detenido un servicio dependiente hasta que el broker está listo.

Coste en tu propio VPS, medido en lugar de citado

Las cifras de rendimiento publicadas se miden en hardware que no tienes, normalmente un servidor multinúcleo con NVMe local. Considéralas un límite superior y mide tu servidor.

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

Ejecuta esas pruebas con el broker inactivo y repítelas con tu tráfico real. La diferencia entre ambas mediciones determina si el broker puede ejecutarse junto a tu aplicación. Para obtener una referencia aproximada del rendimiento mínimo, usa el generador de carga del propio proyecto en lugar de una publicación de blog ajena: nats bench pub test --msgs 100000 --clients 2 para NATS, bin/kafka-producer-perf-test.sh para Kafka y PerfTest para RabbitMQ. Ejecutar el generador en el mismo VPS mide el broker y el generador conjuntamente. Esto es válido siempre que lo indiques al informar de la cifra.

Todos tienen un mismo límite superior. Todas las opciones persistentes esperan a fsync. Por tanto, en un VPS con almacenamiento conectado por red, el disco fija el límite y cambiar de broker no lo modificará.

Tres cargas de trabajo y la cola de mensajes que necesita cada una

  1. Tareas en segundo plano para una aplicación web, como enviar correo electrónico, cambiar el tamaño de imágenes o entregar webhooks. Empiece con Postgres y SKIP LOCKED. Cambie a RabbitMQ con colas de quórum cuando necesite confirmaciones por mensaje, un límite de entregas y una cola de mensajes no entregados que pueda inspeccionar sin implementar esa lógica, o cuando la tabla de tareas se haya convertido en la tabla más activa de la base de datos.
  2. Eventos a los que reaccionan varios servicios internos y que un mensaje más reciente sustituye rápidamente si se pierde. Use Core NATS, con subjects como esquema de enrutamiento y queue groups cuando necesite compartir el trabajo. Añada un stream de JetStream para el conjunto reducido de subjects que deba sobrevivir a un reinicio y deje el resto en memoria.
  3. Un registro de eventos que los consumidores leen desde el principio, para una pista de auditoría, reconstruir un modelo de lectura o alimentar análisis posteriormente. Use JetStream con almacenamiento en archivos y un límite explícito en bytes. Elija Kafka sólo cuando una herramienta posterior requiera el protocolo de Kafka y acepte el heap de la JVM como coste de esa compatibilidad.

El coste de elegir mal en un solo servidor no es el rendimiento. Es la recuperación a las tres de la mañana, cuando necesita saber si los mensajes siguen existiendo. Elija teniendo eso en cuenta.

FAQ

¿Puedo ejecutar Kafka en un VPS de 2 GB?

Arranca, pero los recursos serán muy ajustados. bin/kafka-server-start.sh establece KAFKA_HEAP_OPTS="-Xmx1G -Xms1G" cuando no lo ha sobrescrito, por lo que la JVM reclama 1 GB antes de almacenar ningún mensaje. Kafka también necesita memoria libre adicional para la caché de páginas. Si ejecuta la aplicación y la base de datos en el mismo equipo, empezará a usar swap. Además, tendrá un factor de replicación de 1. Esto significa que acks=all consiste en un único fsync en un solo disco. Por tanto, asumirá el coste operativo de Kafka sin obtener su modelo de durabilidad. NATS JetStream ofrece reproducción durable en el mismo hardware con mucho menos consumo de memoria.

¿Necesito una cola de mensajes si ya ejecuto Postgres?

A menudo, no. Una tabla job leída con SELECT ... FOR UPDATE SKIP LOCKED dentro de una transacción proporciona entrega al menos una vez, workers concurrentes seguros, reintentos y una tabla de mensajes no entregables. No necesita ningún servicio adicional que monitorizar y ya dispone de las copias de seguridad. Los motivos para migrar a otro sistema son concretos: la tabla de la cola se convierte en la carga de escritura más pesada y autovacuum se retrasa; los trabajos de larga duración mantienen abiertas las transacciones y bloquean vacuum en toda la base de datos; o un segundo servicio necesita consumir los mismos eventos de forma independiente.

¿Debo usar NATS JetStream o RabbitMQ para trabajos en segundo plano?

Use RabbitMQ si necesita confirmación por mensaje, un límite de entregas y enrutamiento de mensajes no entregables como funciones integradas. Las colas de quórum siempre son durables. El límite de entregas es 20 de forma predeterminada desde RabbitMQ 4.0. Una política envía los mensajes que superan ese límite a un intercambio de mensajes no entregables que puede vaciar e inspeccionar. Use JetStream si otros consumidores también necesitan reproducir los mismos eventos más adelante, porque un stream conserva los mensajes después de la confirmación, mientras que una cola no lo hace. Con JetStream debe establecer --max-deliver y crear usted mismo la ruta de mensajes no entregables a partir del aviso $JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.>.

¿Cómo puedo saber cuánto retraso tienen mis consumidores?

Cada broker tiene un comando para ello. En RabbitMQ, rabbitmqctl list_queues name messages messages_ready messages_unacknowledged separa el trabajo que espera a un consumidor del trabajo entregado que aún no se ha confirmado. En JetStream, nats consumer report <stream> muestra los mensajes sin procesar y las confirmaciones pendientes de cada consumidor. En Kafka, kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group <group> imprime una columna LAG por partición. Core NATS no tiene mensajes pendientes que consultar porque no almacena nada. En su lugar, monitorice el contador slow_consumers en http://localhost:8222/varz. Este contador registra las conexiones que el servidor cerró porque se estaban quedando atrás, lo que implica pérdida de mensajes.

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