Back to Blog
NATSMicroservicesGolangMessaging

NATS JetStream Patterns for Reliable Microservice Messaging

February 20, 20268 min read

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

go
nc, _ := 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

go
func 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

go
cons, 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
  • NakWithDelay for exponential backoff on processing failures
  • MaxDeliver prevents infinite redelivery loops — set it
  • For IoT pipelines: one stream per device class, one consumer per downstream system