NATS fan-out isn’t about sending a single message to many destinations; it’s about having many independent consumers each receive a copy of the same message.

Let’s see it in action. Imagine we have a service that publishes events whenever a new user signs up. We want to notify a few different downstream systems: a notification service, a data warehousing service, and a real-time analytics dashboard.

Here’s a simplified publisher in Go:

package main

import (
	"log"
	"time"

	"github.com/nats-io/nats.go"
)

func main() {
	// Connect to NATS
	nc, err := nats.Connect(nats.DefaultURL)
	if err != nil {
		log.Fatalf("Failed to connect to NATS: %v", err)
	}
	defer nc.Close()

	log.Println("Connected to NATS. Publishing user signup events...")

	// Simulate user signups
	for i := 1; i <= 5; i++ {
		userID := i
		message := []byte("user_signed_up:" + string(rune(userID)))
		subject := "events.user.signup"

		// Publish the message
		if err := nc.Publish(subject, message); err != nil {
			log.Printf("Error publishing message: %v", err)
		} else {
			log.Printf("Published: '%s' on subject '%s'", message, subject)
		}
		time.Sleep(2 * time.Second) // Wait a bit between events
	}

	// Give NATS time to deliver messages
	nc.Flush()
	log.Println("Finished publishing. Exiting.")
}

And here are three independent consumers, each subscribing to the same subject:

Consumer 1: Notification Service

package main

import (
	"log"
	"time"

	"github.com/nats-io/nats.go"
)

func main() {
	nc, err := nats.Connect(nats.DefaultURL)
	if err != nil {
		log.Fatalf("Failed to connect to NATS: %v", err)
	}
	defer nc.Close()

	subject := "events.user.signup"

	// Subscribe to the subject
	sub, err := nc.Subscribe(subject, func(msg *nats.Msg) {
		log.Printf("Notification Service received message: '%s' on subject '%s'", string(msg.Data), msg.Subject)
		// Simulate sending a notification
		time.Sleep(500 * time.Millisecond)
	})
	if err != nil {
		log.Fatalf("Failed to subscribe to subject '%s': %v", subject, err)
	}
	defer sub.Unsubscribe()

	log.Printf("Notification Service subscribed to '%s'. Waiting for messages...", subject)

	// Keep the subscription alive
	runtime.Goexit()
}

Consumer 2: Data Warehousing Service

package main

import (
	"log"
	"time"

	"github.com/nats-io/nats.go"
)

func main() {
	nc, err := nats.Connect(nats.DefaultURL)
	if err != nil {
		log.Fatalf("Failed to connect to NATS: %v", err)
	}
	defer nc.Close()

	subject := "events.user.signup"

	// Subscribe to the subject
	sub, err := nc.Subscribe(subject, func(msg *nats.Msg) {
		log.Printf("Data Warehousing Service received message: '%s' on subject '%s'", string(msg.Data), msg.Subject)
		// Simulate writing to data warehouse
		time.Sleep(1 * time.Second)
	})
	if err != nil {
		log.Fatalf("Failed to subscribe to subject '%s': %v", subject, err)
	}
	defer sub.Unsubscribe()

	log.Printf("Data Warehousing Service subscribed to '%s'. Waiting for messages...", subject)

	// Keep the subscription alive
	runtime.Goexit()
}

Consumer 3: Real-time Analytics Dashboard

package main

import (
	"log"
	"time"

	"github.com/nats-io/nats.go"
)

func main() {
	nc, err := nats.Connect(nats.DefaultURL)
	if err != nil {
		log.Fatalf("Failed to connect to NATS: %v", err)
	}
	defer nc.Close()

	subject := "events.user.signup"

	// Subscribe to the subject
	sub, err := nc.Subscribe(subject, func(msg *nats.Msg) {
		log.Printf("Analytics Dashboard received message: '%s' on subject '%s'", string(msg.Data), msg.Subject)
		// Simulate updating dashboard
		time.Sleep(200 * time.Millisecond)
	})
	if err != nil {
		log.Fatalf("Failed to subscribe to subject '%s': %v", subject, err)
	}
	defer sub.Unsubscribe()

	log.Printf("Analytics Dashboard subscribed to '%s'. Waiting for messages...", subject)

	// Keep the subscription alive
	runtime.Goexit()
}

When the publisher sends a message to events.user.signup, NATS delivers a copy of that message to each of these three subscribers. Each subscriber processes the message independently. This is the core of fan-out: one publish, multiple independent deliveries.

The magic happens because NATS treats each Subscribe call as a distinct client request. When a message arrives on a subject, NATS checks its internal routing table to see which clients have active subscriptions to that subject. For every active subscription, it then forwards a copy of the message. There’s no single "group" receiving the message; it’s a series of individual deliveries orchestrated by the NATS server based on active subscriptions.

The problem this solves is decoupling producers from consumers. The publisher doesn’t need to know who is listening or how many are listening. It just broadcasts its events. Downstream services can subscribe or unsubscribe dynamically without affecting the publisher or other consumers. This makes systems highly flexible and scalable. If you need to add a fourth service to react to user signups, you just deploy it with a new nc.Subscribe("events.user.signup", ...) call.

The primary lever you control is the subject name. By using specific, hierarchical subject names (e.g., events.user.signup, metrics.cpu.usage, orders.created.international), you can route messages precisely. Consumers subscribe to the subjects they care about. NATS’s wildcard matching (* for a single token, > for multiple tokens) allows for flexible subscription patterns, but for a simple fan-out to known consumers, using the exact subject is standard.

The one thing most people don’t realize is how NATS handles message ordering and delivery guarantees in this scenario. For standard Subscribe calls (not queue subscribers), each message is delivered to every subscriber. If a subscriber is slow or temporarily disconnected, it will miss messages published while it was unavailable. NATS doesn’t buffer messages for standard subscribers indefinitely. You’re relying on the consumer to be available and process messages at a rate that keeps up with the publisher. If you need guaranteed delivery or at-least-once processing for individual consumers, you’d typically use NATS JetStream, which provides persistence and acknowledgments.

The next step is understanding how to manage message delivery guarantees for individual consumers when using fan-out.

Want structured learning?

Take the full Nats course →