Core NATS es la capa ligera de mensajería que está detrás de la mayoría de los despliegues de NATS. Enruta mensajes por subject, los distribuye mediante publish/subscribe, admite llamadas al estilo RPC a través de request/reply y balancea la carga de trabajo mediante queue groups, todo con entrega at-most-once y fire-and-forget y sin persistencia en el servidor.
Este último punto importa. Si un suscriptor está desconectado cuando se publica un mensaje, el mensaje se pierde. Core NATS es rápido y desacoplado, pero no ofrece durabilidad. Cuando necesitas replay, retención o reentrega garantizada, das el paso a JetStream (Parte 2).
Para investigar este artículo se consultó docs.nats.io el July 6, 2026.
TL;DR
- Core NATS es pub/sub basado en interés: los mensajes se entregan solo a suscriptores activos cuyos subjects coinciden; si nadie está escuchando, el mensaje se descarta.
- La entrega es best-effort, at-most-once: los mensajes se mantienen en memoria y nunca se escriben en disco.
- Los subjects son cadenas jerárquicas separadas por tokens
.; los wildcards (*= un token,>= uno o más tokens al final) se aplican al suscribirse, no al publicar. - Request/reply ofrece semántica de llamadas al estilo RPC sobre pub/sub asíncrono: el cliente publica una solicitud con un inbox de respuesta y se bloquea hasta que llega una respuesta o se agota el timeout.
- Los queue groups balancean la carga entre los miembros de un mismo grupo; los suscriptores normales (fuera de un queue group) del mismo subject siguen recibiendo todos los mensajes.
- Usa Core NATS para fan-out en tiempo real, RPC interno y pools de workers. Usa JetStream cuando los eventos deban sobrevivir al tiempo que un suscriptor esté desconectado.
Qué aprenderás aquí
- Qué es Core NATS y qué no es
- Cómo funcionan las jerarquías de subjects y los wildcards, incluidos los espacios de nombres reservados con
$ - La semántica de fan-out de publish/subscribe
- Request/reply como RPC sobre mensajería
- Queue groups para balancear la carga
- Una guía para elegir patrones: síntoma → patrón → garantía de entrega
- Cuándo pasar de Core NATS a JetStream
Para entender dónde encaja NATS en la arquitectura de un sistema en crecimiento, consulta Las piezas del diseño de software moderno y eficaz.
Qué es Core NATS (y qué no es)
NATS separa a los publicadores (quienes envían mensajes) de los suscriptores (quienes los reciben). El servidor enruta por nombre de subject: no hay un paso para crear topics en el broker ni se requiere un registro de esquemas a nivel de protocolo.
Características de Core NATS, según la documentación oficial:
| Propiedad | Comportamiento de Core NATS |
|---|---|
| Garantía de entrega | Best-effort, at-most-once |
| Almacenamiento | Solo en memoria; nunca se escribe en disco |
| Requisito de suscripción | Basado en interés: si no hay suscriptores activos, el mensaje se descarta |
| Fan-out | Todos los suscriptores normales que coincidan reciben el mensaje |
| Replay | No disponible |
JetStream (Parte 2) añade persistencia, consumidores durables y entrega at-least-once sobre este transporte.
Subjects y wildcards
Un subject es una cadena separada por puntos que identifica un canal de mensajes. El token . crea la jerarquía:
orders.created.us-east
orders.created.eu-west
orders.shipped.us-east
Reglas de los wildcards
Los publicadores siempre usan subjects especificados por completo: no se usan wildcards al publicar. Los suscriptores usan wildcards para definir su ámbito de interés:
| Wildcard | Coincide con | Ejemplo de suscripción | Coincide | No coincide |
|---|---|---|---|---|
* | Exactamente un token | orders.*.us-east | orders.created.us-east | orders.created.eu-west |
> | Uno o más tokens al final; debe ser el último | orders.> | orders.created.us-east | billing.invoice |
Los wildcards se parecen a las expresiones regulares, pero son más restrictivos: * no puede coincidir con una parte de un token y > debe ser el último token.
Subjects reservados con $
Los subjects que empiezan con $ están reservados para uso del sistema: $SYS, $JS, $KV y otros. El tráfico de la aplicación no debe usar prefijos $, a menos que interactúes intencionalmente con un subsistema de NATS.
JetStream KV (presentado en la Parte 3) almacena claves bajo subjects como $KV.<bucket>.<key>. Las reglas de wildcards de Core NATS se aplican al observar un bucket:
Bucket: app-objects
Key: orders-v1.abc-123
Subject: $KV.app-objects.orders-v1.abc-123
Watch all order keys:
Subscribe: $KV.app-objects.orders-v1.>
En detalle: $KV está reservado, app-objects es el nombre del bucket, orders-v1 es un prefijo de clave (tipo de entidad) y > coincide con todos los ID de órdenes bajo ese prefijo.
Publish/Subscribe: fan-out fire-and-forget
Pub/sub es el patrón predeterminado de Core NATS. Un publicador, muchos suscriptores y ningún protocolo de confirmación.
import { connect, StringCodec } from "nats";
const nc = await connect({ servers: "nats://localhost:4222" });
const sc = StringCodec();
// Two independent consumers — both get every message
nc.subscribe("orders.created", {
callback: (_err, msg) => console.log("analytics:", sc.decode(msg.data)),
});
nc.subscribe("orders.created", {
callback: (_err, msg) => console.log("email:", sc.decode(msg.data)),
});
await nc.publish("orders.created", sc.encode(JSON.stringify({ id: "ord-42" })));
await nc.flush();
Punto clave: Si ninguno de los suscriptores está conectado cuando se publica el mensaje, este se descarta. No hay replay, cola de mensajes fallidos ni ack. Diseña tu sistema teniendo esto en cuenta, o usa JetStream.
┌─────────────────────────────────────┐
│ NATS Server │
│ (in-memory, no persistence) │
└─────────────────────────────────────┘
▲ ▲
publish │ │
orders.created ────────┘ │
│
┌─────────────────────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
[Analytics] [Email] [no subscriber]
plain sub plain sub → message lost
gets ALL gets ALL
Request/Reply: RPC sobre Pub/Sub
Request/reply no es un protocolo separado. Es pub/sub con una dirección de respuesta:
- El cliente publica una solicitud en un subject y establece un inbox único como subject de respuesta.
- Un servicio que responde recibe la solicitud y publica la respuesta en ese inbox.
- Las bibliotecas cliente encapsulan esto en una llamada bloqueante a
request()con un timeout.
// Client — RPC-style call with timeout
msg, err := nc.Request("billing.invoice.get", []byte(`{"id":"inv-99"}`), time.Second)
if err == nats.ErrNoResponders {
// No service listening — fail fast (503 when no_responders is enabled)
log.Fatal("no billing service available")
}
fmt.Println(string(msg.Data))
// Responder
nc.Subscribe("billing.invoice.get", func(msg *nats.Msg) {
// ... look up invoice ...
msg.Respond([]byte(`{"id":"inv-99","total":1500}`))
})
Una distinción importante: Esto ofrece semántica de llamadas al estilo RPC. El transporte sigue siendo pub/sub asíncrono. Tú eres responsable de gestionar los timeouts, ErrNoResponders y la disponibilidad de los servicios que responden.
Varios servicios que responden pueden unirse a un queue group en el subject de solicitud para escalar horizontalmente: un miembro elegido al azar procesa cada solicitud.
Queue groups: balanceo de carga integrado
Los queue groups distribuyen el trabajo entre un conjunto de suscriptores que comparten el mismo nombre de cola. NATS entrega cada mensaje a exactamente un miembro del grupo, elegido al azar.
// Three workers sharing the same queue — each message goes to one worker
for (let i = 0; i < 3; i++) {
nc.subscribe("jobs.render", {
queue: "processors",
callback: (_err, msg) => console.log(`worker-${i}:`, sc.decode(msg.data)),
});
}
Detalle crítico: Los queue groups y los suscriptores normales coexisten en el mismo subject.
Subject: jobs.render
[Worker A] queue:"processors" ─┐
[Worker B] queue:"processors" ─┼─ each message → ONE queue member
[Worker C] queue:"processors" ─┘
[Monitor] plain subscriber ─── every message (for observability)
Si agregas un suscriptor normal para monitoreo junto con los workers de la cola, este sigue recibiendo todos los mensajes. Solo los miembros de un mismo queue group se reparten el trabajo.
Los queue groups son dinámicos: no requieren configuración en el servidor. Escala iniciando o deteniendo instancias de la aplicación. Usa drain antes de salir para que los mensajes en procesamiento se completen al reducir la escala.
Cuándo usar cada primitiva
| Síntoma | Patrón de Core NATS | Garantía de entrega |
|---|---|---|
| Notificar a varios servicios cuando ocurre algo | Fan-out con pub/sub | At-most-once; los suscriptores desconectados pierden mensajes |
| Llamar a un servicio y esperar una respuesta | Request/reply | At-most-once; timeout si no hay quien responda |
| Distribuir tareas entre N workers | Queue group | At-most-once; un worker por mensaje |
| No perder nunca un evento | No Core NATS | → JetStream Parte 2 |
| Reproducir eventos después de corregir un bug | No Core NATS | → Consumidores durables de JetStream |
Implementación concreta: elegir un patrón
Este es un pequeño servicio que usa las tres primitivas para un flujo de órdenes:
import { connect, StringCodec } from "nats";
const nc = await connect({ servers: process.env.NATS_URL });
const sc = StringCodec();
// 1. Fan-out: notify analytics and email on every order
nc.subscribe("orders.created", {
callback: (_err, msg) => publishToAnalytics(JSON.parse(sc.decode(msg.data))),
});
// 2. Load-balance: PDF generation across workers
nc.subscribe("orders.pdf", {
queue: "pdf-workers",
callback: async (_err, msg) => {
const order = JSON.parse(sc.decode(msg.data));
await renderPdf(order);
},
});
// 3. RPC: look up inventory synchronously during checkout
nc.subscribe("inventory.check", {
queue: "inventory-service",
callback: (_err, msg) => {
const sku = JSON.parse(sc.decode(msg.data));
const available = lookupStock(sku);
msg.respond(sc.encode(JSON.stringify({ available })));
},
});
// Checkout flow
async function checkout(cart: Cart) {
const stock = await nc.request(
"inventory.check",
sc.encode(JSON.stringify(cart.items)),
{ timeout: 2000 },
);
// ... charge, create order ...
await nc.publish("orders.created", sc.encode(JSON.stringify({ id: "ord-99" })));
await nc.publish("orders.pdf", sc.encode(JSON.stringify({ id: "ord-99" })));
}
Nota de diseño: Como Core NATS es at-most-once, publicar en orders.created después de un cobro exitoso es un punto de riesgo. Si ningún suscriptor está escuchando, se pierde la notificación. Para eventos críticos para el negocio, publica mediante JetStream.
Errores comunes
- Dar por sentada la entrega. Core NATS no almacena mensajes en búfer para suscriptores desconectados. Si esto importa, usa JetStream.
- Tratar los queue groups como exclusivos. Los suscriptores normales del mismo subject siguen recibiendo todos los mensajes.
- Usar wildcards al publicar. Los publicadores deben usar subjects concretos; los wildcards se usan solo al suscribirse.
- Confundir request/reply con HTTP. No hay una capa integrada de códigos de estado más allá de
ErrNoResponders. Los timeouts, los reintentos y las estructuras de error son responsabilidad de la aplicación. - Omitir la idempotencia. At-most-once también puede implicar duplicados en escenarios de falla en la capa de aplicación. Diseña los handlers para tolerar reintentos, incluso con Core NATS.
Core NATS vs JetStream: cuándo dar el paso
Need Core NATS JetStream
─────────────────────────────────────────────────────────────
Live fan-out, all online ✓ optional
RPC with timeout ✓ —
Worker pool load balancing ✓ (queue groups) ✓ (work-queue streams)
Subscriber was offline ✗ message lost ✓ replay from stream
Guaranteed processing ✗ ✓ ack + redeliver
Key-value shared state ✗ ✓ KV buckets (Part 3)
Siguiente en la serie: NATS JetStream: cuando los eventos necesitan sobrevivir al momento
Fuentes
- Core NATS — modelo pub/sub y entrega at-most-once
- ¿Qué es NATS? — QoS y semántica fire-and-forget
- Publish-Subscribe — fan-out y estructura de mensajes
- Mensajería basada en subjects — jerarquía, wildcards y nombres reservados
- Request-Reply — patrón de inbox y ausencia de servicios que respondan
- Queue Groups — semántica de balanceo de carga
- Semántica de Request-Reply (guía para desarrolladores)
- Suscripciones a colas (guía para desarrolladores)
- nats-io/nats.go — plantillas de subjects en kv.go — estructura de
$KV.<bucket>.>