Core NATS es excelente para distribuir mensajes en tiempo real a múltiples destinatarios, pero no puede garantizar la entrega si un suscriptor está desconectado. JetStream agrega streams durables (almacenes de mensajes) y consumidores (máquinas de estados de entrega) para que productores y consumidores puedan desacoplarse en el tiempo: los mensajes se capturan, se reproducen bajo demanda y se reentregan hasta recibir un acuse.
Este artículo explica cómo funcionan los streams y consumidores en la práctica, con advertencias claras sobre la durabilidad para tomar decisiones en producción.
Requisito previo: Core NATS (Parte 1). Investigación basada en docs.nats.io el July 6, 2026.
TL;DR
- JetStream agrega entrega al menos una vez (exactamente una vez es opcional) sobre el transporte de Core NATS.
- Un stream es un almacén durable de mensajes vinculado a uno o más subjects; captura los mensajes publicados en los subjects que coinciden.
- Un consumidor es una vista con estado de un stream: lleva registro de qué mensajes se entregaron y recibieron un acuse.
- Los consumidores pull durables son la opción predeterminada recomendada para proyectos nuevos: ack explícito, control de flujo y obtención escalable de lotes.
MaxDeliverno es una cola de mensajes fallidos: después del máximo de intentos, NATS deja de reentregar el mensaje, pero este permanece en el stream.- Diseña consumidores idempotentes sin importar la política de ack.
Qué aprenderás aquí
- Qué agrega JetStream a Core NATS
- Cómo funcionan los streams: vinculación de subjects, políticas de retención y límites
- Cómo funcionan los consumidores: durables frente a efímeros, push frente a pull, filtros y ajuste de ack
- Una guía rápida de configuración de consumidores (
AckWait,MaxDeliver,MaxAckPending) - Un ejemplo de extremo a extremo con outbox → JetStream → proyector
- Cómo se relacionan los buckets KV con los streams (
KV_app-objects) - Advertencias operativas sobre durabilidad en producción
Core NATS frente a JetStream
| Aspecto | Core NATS | JetStream |
|---|---|---|
| Entrega | Como máximo una vez, sin garantías | Al menos una vez por defecto; exactamente una vez es opcional |
| Almacenamiento | En memoria, sin disco | En archivo o en memoria, con retención configurable |
| Suscriptores desconectados | El mensaje se pierde | Reproducción desde el stream al reconectarse |
| Acoplamiento temporal | El publicador y el suscriptor deben coincidir en el tiempo | Desacoplados por diseño |
| Reproducción | No disponible | Entrega a partir de una secuencia, una marca de tiempo o la última posición |
Core NATS JetStream
───────── ─────────
Publisher ──► Subscriber Publisher ──► Stream ──► Consumer ──► Worker
(must be online) (persisted) (tracks cursor)
JetStream está integrado en nats-server. Actívalo en la configuración del servidor y usa las API de publicación de JetStream: el servidor confirma cuando el mensaje se almacena correctamente.
Streams: el registro durable
Un stream es un almacén de mensajes que se vincula a uno o más subjects (admite comodines). Los mensajes publicados en los subjects que coinciden se capturan según las políticas de retención y los límites.
Crear un stream
nats stream add ORDERS \
--subjects "orders.>" \
--retention limits \
--storage file \
--replicas 3 \
--max-age 7d \
--discard old
| Configuración | Qué controla |
|---|---|
subjects | Qué mensajes publicados se capturan |
retention | limits (predeterminado, reproducción), workqueue (eliminar al recibir un ack), interest (retener mientras los consumidores tengan interés) |
storage | file (predeterminado) o memory |
replicas | 1–5 para clustering y alta disponibilidad |
max_age / max_msgs / max_bytes | Límites de retención |
discard | old (predeterminado) o new cuando se alcanzan los límites |
Prefiere las API de publicación de JetStream a la publicación directa de Core NATS: el servidor devuelve un acuse que confirma que el mensaje se almacenó.
Los buckets KV son streams
Los buckets KV de JetStream se implementan como streams llamados KV_<bucket>. Un bucket app-objects se materializa como el stream KV_app-objects:
nats kv add app-objects --history 5 --ttl 1h
nats stream info KV_app-objects # subjects: $KV.app-objects.>
Esto importa porque KV hereda los límites, la replicación y el comportamiento de almacenamiento de los streams. La Parte 3 explica la abstracción KV; por ahora, recuerda que un bucket es un stream con una vista del valor más reciente.
Consumidores: máquinas de estados de entrega
Un consumidor es una vista con estado de un stream. Lleva registro de qué mensajes se entregaron y recibieron un acuse. Los mensajes sin acuse se reentregan cuando vence AckWait.
Durables frente a efímeros
| Tipo | Comportamiento | Cuándo usarlo |
|---|---|---|
| Durable | Estado persistente; sobrevive a reinicios; cursor compartido por nombre | Workers y proyectores en producción |
| Efímero | Sin estado persistente; se elimina automáticamente tras un período de inactividad | Depuración, reproducción puntual, dashboards temporales |
Define siempre un durable_name para los consumidores de producción.
Push frente a pull
| Tipo | Comportamiento | Recomendación |
|---|---|---|
| Pull | El cliente solicita lotes bajo demanda | Opción predeterminada para proyectos nuevos: mejor control de flujo, escalabilidad y manejo de errores |
| Push | El servidor entrega a un DeliverSubject | Integraciones heredadas; MaxAckPending es el único mecanismo de control de flujo |
FilterSubjects
Los consumidores pueden filtrar qué mensajes del stream reciben:
{
"durable_name": "search-projector",
"filter_subject": "orders.created",
"ack_policy": "explicit",
"ack_wait": 30000000000,
"max_deliver": 5,
"max_ack_pending": 100,
"deliver_policy": "all"
}
El stream captura orders.>, pero este consumidor solo ve orders.created. Un consumidor por cada necesidad de los sistemas downstream mantiene independientes a los proyectores.
Guía rápida para ajustar ack
| Parámetro | Qué controla | Sugerencia de ajuste |
|---|---|---|
AckWait | Tiempo de espera para la reentrega después de la entrega | Auméntalo para API externas lentas (por ejemplo, operaciones bulk de OpenSearch) |
MaxDeliver | Máximo de intentos de reentrega por mensaje | Define un límite finito y una alerta; planifica la reproducción manual de mensajes problemáticos |
MaxAckPending | Límite de mensajes en tránsito sin ack | Redúcelo para workers con alta latencia; principal control de flujo para push |
DeliverPolicy | Desde dónde comienza la reproducción | DeliverNew para mensajes en tiempo real; DeliverAll para procesamiento histórico |
BackOff | Intervalos crecientes entre reentregas | Tiene prioridad sobre AckWait cuando está configurado |
Advertencia operativa: Después de MaxDeliver intentos, NATS deja de reentregar el mensaje, pero este permanece en el stream. No hay una cola de mensajes fallidos automática. Tu proceso operativo o aplicación debe mover o eliminar estos mensajes, o generar una alerta.
PUBLISH PATH
------------
Producer App
|
| JetStream publish (orders.created)
v
+------------------+ +---------------------------+
| NATS Server | -------------------------> | Stream: ORDERS |
| (JetStream) | captures orders.> | retention: limits |
+------------------+ | storage: file, R=3 |
^ | [msg1][msg2][msg3]... |
| publish ack +---------------------------+
| |
| | N consumer views
| +---------------------+---------------------+
| | | |
| +---------v---------+ +---------v---------+
| | Consumer (durable) | | Consumer (durable) |
| | pull, filter: | | pull, filter: |
| | orders.created | | orders.shipped |
| | MaxAckPending: 100 | | MaxDeliver: 5 |
| +---------+---------+ +---------+---------+
| | |
| v v
| Search Projector Shipping Worker
|
+--- if no ack within AckWait --> redeliver (up to MaxDeliver)
Implementación concreta: outbox → JetStream → proyector
Esto amplía el pipeline del modelo de lectura de Las piezas del diseño de software moderno y eficaz con semánticas de publicación y consumo específicas de JetStream.
Flujo de escritura: almacena el cambio de negocio y la fila del outbox en una sola transacción:
BEGIN;
INSERT INTO todos (id, user_id, title, completed)
VALUES (:id, :user_id, :title, false);
INSERT INTO outbox_events (id, topic, payload, created_at)
VALUES (:event_id, 'todo.created', :payload, now());
COMMIT;
Publicador: lee el outbox, publica en JetStream y marca como publicado:
import { connect, StringCodec } from "nats";
const nc = await connect({ servers: process.env.NATS_URL });
const js = nc.jetstream();
const sc = StringCodec();
async function publishOutboxBatch(events: OutboxEvent[]) {
for (const event of events) {
const ack = await js.publish(event.topic, sc.encode(JSON.stringify({
id: event.id,
occurredAt: new Date().toISOString(),
data: event.payload,
})));
// Server confirmed storage in stream
console.log(`stored seq=${ack.seq} stream=${ack.stream}`);
await markPublished(event.id);
}
}
Proyector: consumidor pull durable con ack explícito:
const sub = await js.pullSubscribe("todo.created", {
config: { durable_name: "search-projector" },
stream: "TODOS",
});
for (;;) {
const msgs = await sub.fetch(10, { expires: 5000 });
for (const msg of msgs) {
const event = JSON.parse(sc.decode(msg.data));
if (await alreadyProcessed(event.id)) {
msg.ack();
continue;
}
try {
await openSearch.index({
index: "todos",
id: event.data.todo_id,
document: {
title: event.data.title,
owner_id: event.data.user_id,
completed: false,
},
});
await markProcessed(event.id);
msg.ack();
} catch (err) {
msg.nak(); // redeliver after AckWait
}
}
}
La estructura se mantiene estable entre distintos stacks:
- Escribe una sola vez en la fuente de verdad
- Publica los cambios de forma confiable a través de JetStream
- Actualiza los modelos de lectura en segundo plano
- Haz que los proyectores sean idempotentes (elimina duplicados por
event.id) - Mide el retraso entre la escritura y la visibilidad en lectura
Advertencias sobre durabilidad (lee esto antes de pasar a producción)
JetStream es durable, no mágico. Pon en contexto las afirmaciones sobre durabilidad en las revisiones operativas y los análisis posteriores a incidentes:
| Advertencia | Detalle |
|---|---|
| Entrega predeterminada | Al menos una vez: diseña consumidores idempotentes |
| Exactamente una vez | Es opcional mediante IDs de mensaje del publicador y deduplicación; aun así, contempla duplicados poco frecuentes |
| fsync en disco | El sync_interval predeterminado es de 2 minutos: un ack no siempre significa que el mensaje esté inmediatamente en disco ante todos los tipos de falla |
MaxDeliver agotado | El mensaje permanece en el stream; no se enruta automáticamente a una DLQ |
| Acks tardíos | Un ack enviado después de que el mensaje se reentregó puede ignorarse; maneja los errores antes de enviar el ack |
Elegir entre Core NATS y JetStream
| Necesidad | Core NATS | Stream de JetStream | Stream de cola de trabajo |
|---|---|---|---|
| Distribución en tiempo real a múltiples destinatarios, todos los suscriptores conectados | ✓ | opcional | — |
| El suscriptor estaba desconectado | ✗ | ✓ reproducción | ✓ |
| Procesamiento garantizado con reintentos | ✗ | ✓ ack + reentrega | ✓ |
| Cada mensaje procesado exactamente una vez | ✗ | consumidores idempotentes | ✓ (con salvedades) |
| Cola de trabajos compartida | grupos de cola | — | ✓ retención workqueue |
Errores comunes
- Tratar el ack como “trabajo completado en disco”. Comprende la replicación y
sync_intervalsegún tus requisitos de RPO. - Suponer que
MaxDeliver= DLQ. Planifica el manejo explícito de mensajes problemáticos. - Usar consumidores push por defecto. Los consumidores pull ofrecen mejor control de flujo para servicios nuevos.
- Un solo consumidor para todo. Usa
FilterSubjectpara darle a cada proyector su propio cursor y política de reintentos. - Omitir la idempotencia. La entrega al menos una vez implica que los duplicados son normales, no excepcionales.
Siguiente en la serie: JetStream KV es un stream de cambios, no una tabla
Fuentes
- JetStream — persistencia, reproducción, semántica de entrega exactamente una vez
- Core NATS — comportamiento base de entrega como máximo una vez
- Streams — retención, límites, almacenamiento
- Consumidores — durables/efímeros, push/pull, configuración de ack
- Almacén clave-valor — buckets KV como streams
- Las piezas del diseño de software moderno y eficaz — pipeline de outbox → proyector