Why NATS Core Is Not Enough for Production
NATS core publish-subscribe is extremely fast (10M+ msgs/sec). But it's fire-and-forget:
- No persistence — if a subscriber is down, the message is lost
- No replay — new consumers can't catch up on missed events
- No delivery guarantees
JetStream adds persistence, replay, and consumer acknowledgement on top of NATS — without the operational complexity of Kafka.
Setting Up a Stream
gonc, _ := nats.Connect(nats.DefaultURL) js, _ := jetstream.New(nc) ctx := context.Background() stream, err := js.CreateOrUpdateStream(ctx, jetstream.StreamConfig{ Name: "METRICS", Subjects: []string{"metrics.>"}, Retention: jetstream.LimitsPolicy, MaxAge: 24 * time.Hour, Storage: jetstream.FileStorage, Replicas: 3, })
Publishing with Ack
gofunc publishMetric(ctx context.Context, js jetstream.JetStream, m Metric) error { data, err := json.Marshal(m) if err != nil { return fmt.Errorf("marshal: %w", err) } ack, err := js.Publish(ctx, "metrics."+m.DeviceID, data) if err != nil { return fmt.Errorf("publish: %w", err) } // Ack confirms the message was persisted to the stream _ = ack.Sequence return nil }
Durable Consumer with Manual Ack
gocons, err := stream.CreateOrUpdateConsumer(ctx, jetstream.ConsumerConfig{ Durable: "metrics-processor", AckPolicy: jetstream.AckExplicitPolicy, MaxDeliver: 5, AckWait: 30 * time.Second, FilterSubject: "metrics.>", }) // Pull-based consumption — more control than push msgs, _ := cons.Messages() for { msg, err := msgs.Next() if err != nil { break } if err := processMetric(ctx, msg.Data()); err != nil { // Nack with backoff — will be redelivered msg.NakWithDelay(5 * time.Second) continue } msg.Ack() }
Fan-Out Pattern: Multiple Independent Consumers
go// Each consumer gets its OWN copy of every message // Perfect for: processor + archiver + alerting all consuming the same stream for _, name := range []string{"processor", "archiver", "alerting"} { _, err := stream.CreateOrUpdateConsumer(ctx, jetstream.ConsumerConfig{ Durable: name, AckPolicy: jetstream.AckExplicitPolicy, }) if err != nil { log.Fatalf("create consumer %s: %v", name, err) } }
This is fundamentally different from a queue (competing consumers) — each durable consumer gets all messages independently.
Key Takeaways
- JetStream = NATS + persistence + delivery guarantees
- Pull consumers > push consumers for backpressure control
NakWithDelayfor exponential backoff on processing failuresMaxDeliverprevents infinite redelivery loops — set it- For IoT pipelines: one stream per device class, one consumer per downstream system