Mirror streams in NATS JetStream give you one-way, asynchronous replication of stream data. Think of it like a one-way mirror—data flows in from a single origin stream, automatically and continuously. No complex synchronization, no manual syncing, no fuss.
You’ve got a stream of data in one region, but consumers in another region need access to it. Or maybe you need to replicate data across NATS accounts. Or you just want a read-only replica to spread the load so you’re not hammering a single stream.
Mirror streams solve all of these. They give you a faithful, read-only copy of your origin stream that stays in sync automatically—even across unreliable network connections.
Configuration is all done on the receiving side. Your origin stream doesn’t need any special setup—it won’t even see a consumer created for the replication.
The mirror relationship is strict: each mirror can only replicate from a single origin stream, and mirrors are read-only. Clients can’t publish directly to them. But consumers have full capabilities, and you can still delete messages from the mirror independently if needed.
When setting up a mirror, the only required field is the name of the origin stream. But you’ve got several optional parameters:
One thing to note: you can’t use filter subject and subject transforms together—pick one.
mirror_direct so clients read from mirror replicas instead of routing back to the originThis is important to understand:
Mirrors are generally tolerant of the origin being temporarily unreachable. When the connection drops, the mirror waits about 10–20 seconds before retrying.
The cluster setup we use in this example.

Start by watching the stream report so you can see changes in real time:
watch nats str reportCreate a primary stream in cluster 1:
nats str add events \ --cluster c1 \ --subjects 'events.>' \ --defaultsCreate a mirror of that stream in cluster 2:
nats str add events2 \ --cluster c2 \ --mirror events \ --defaultsThe stream report will show both streams, with the mirror appearing in the replication section.
Publish some messages to the primary:
nats pub events.order '{"id": 1, "item": "widget"}'nats pub events.order '{"id": 2, "item": "gadget"}'nats pub events.order '{"id": 3, "item": "gizmo"}'Check the stream report—both streams should show three messages. Read the last message from the mirror to confirm:
nats str get events2 --last-for 'events.>'Try publishing directly to the mirror—this fails because mirrors have no bound subjects and cannot accept direct publishes:
nats req events.order '{"id": 4, "item": "doohickey"}' -H Nats-Expected-Stream:events2Publish to the primary instead—this works:
nats req events.order '{"id": 4, "item": "doohickey"}' -H Nats-Expected-Stream:eventsCompare the state of both streams:
nats str info events -j | jq '.state'nats str info events2 -j | jq '.state'Sequence numbers and timestamps match. The mirror is a faithful copy.
Delete a message from the origin:
nats str rmm events 2 --forceThe mirror still has it:
nats str get events2 2Delete a message from the mirror:
nats str rmm events2 1 --forceThe origin still has it:
nats str get events 1Deletes on one side don’t affect the other. This makes mirrors useful as independent backups.
NATS 2.12 introduced mirror promotion—if your origin stream goes down permanently, you can promote a mirror to become the new primary.
The process:
Keep in mind: the server won’t verify if the mirror is fully caught up, so any unreplicated messages will be lost.
Start a continuous publisher sending messages to the primary. Using nats bench ensures automatic retries on failed publishes:
nats bench js pub sync \ --stream events \ --multisubject \ --sleep 500ms 'events'Messages flow into the primary and replicate to the mirror in real time.
Now, unbind subjects from the primary by setting them to $devnull—a special placeholder that keeps the stream valid but stops it from accepting new publishes on the original subjects:
nats str edit events \ --subjects '$devnull' \ --forceThen promote the mirror by removing its mirror configuration and binding the original subjects:
nats str edit events2 \ --no-mirror \ --subjects 'events.>' \ --forceAnd just like that, events2 is now the new primary stream accepting writes directly. The publisher reconnects and messages start flowing into events2.
Note: there’s a brief window between unbinding the old primary and promoting the mirror where publishes will fail. Your clients need retry logic to handle this gracefully.
You can now clean up the old stream:
nats str rm events --forceHere’s a Go program that demonstrates mirror streams end-to-end—replication, metadata preservation, read-only behavior, and independent deletes:
1package main2
3import (4 "context"5 "fmt"6 "log"7 "time"8
9 "github.com/nats-io/nats.go"10 "github.com/nats-io/nats.go/jetstream"11)12
13func main() {14 nc, err := nats.Connect(nats.DefaultURL)15 if err != nil {16 log.Fatal(err)17 }18 defer nc.Close()19
20 js, err := jetstream.New(nc)21 if err != nil {22 log.Fatal(err)23 }24
25 ctx := context.Background()26
27 // Clean up previous runs28 _ = js.DeleteStream(ctx, "EVENTS")29 _ = js.DeleteStream(ctx, "EVENTS_MIRROR")30
31 // --- Setup ---32 fmt.Println("--- Setup ---")33
34 _, err = js.CreateStream(ctx, jetstream.StreamConfig{35 Name: "EVENTS",36 Subjects: []string{"events.>"},37 })38 if err != nil {39 log.Fatal(err)40 }41 fmt.Println("Created stream: EVENTS (subjects: events.>)")42
43 _, err = js.CreateStream(ctx, jetstream.StreamConfig{44 Name: "EVENTS_MIRROR",45 Mirror: &jetstream.StreamSource{46 Name: "EVENTS",47 },48 })49 if err != nil {50 log.Fatal(err)51 }52 fmt.Println("Created mirror: EVENTS_MIRROR")53
54 // --- Publish and Verify Replication ---55 fmt.Println("\n--- Replication ---")56
57 for i := 1; i <= 5; i++ {58 ack, err := js.Publish(ctx, fmt.Sprintf("events.order.%d", i),59 fmt.Appendf(nil, `{"id": %d}`, i))60 if err != nil {61 log.Fatal(err)62 }63 fmt.Printf(" Published seq %d to EVENTS\n", ack.Sequence)64 }65
66 // Give the mirror time to replicate67 time.Sleep(time.Second)68
69 mirror, err := js.Stream(ctx, "EVENTS_MIRROR")70 if err != nil {71 log.Fatal(err)72 }73
74 fmt.Println(" Reading from mirror:")75 for seq := uint64(1); seq <= 5; seq++ {76 msg, err := mirror.GetMsg(ctx, seq)77 if err != nil {78 fmt.Printf(" seq %d — error: %v\n", seq, err)79 continue80 }81 fmt.Printf(" seq %d — %s — %s\n", msg.Sequence, msg.Subject, string(msg.Data))82 }83
84 // --- Metadata Preservation ---85 fmt.Println("\n--- Metadata Preservation ---")86
87 primary, err := js.Stream(ctx, "EVENTS")88 if err != nil {89 log.Fatal(err)90 }91
92 for seq := uint64(1); seq <= 3; seq++ {93 pMsg, _ := primary.GetMsg(ctx, seq)94 mMsg, _ := mirror.GetMsg(ctx, seq)95 fmt.Printf(" Seq %d — primary: %s | mirror: %s | match: %v\n",96 seq,97 pMsg.Time.Format(time.RFC3339Nano),98 mMsg.Time.Format(time.RFC3339Nano),99 pMsg.Time.Equal(mMsg.Time))100 }101
102 // --- Read-Only ---103 fmt.Println("\n--- Read-Only ---")104
105 mirrorInfo, _ := mirror.Info(ctx)106 fmt.Printf(" Mirror subjects: %v (empty — clients can't publish directly)\n",107 mirrorInfo.Config.Subjects)108
109 // --- Delete Behavior ---110 fmt.Println("\n--- Delete Behavior ---")111
112 fmt.Println(" Deleting seq 2 from primary...")113 if err := primary.DeleteMsg(ctx, 2); err != nil {114 log.Fatal(err)115 }116
117 _, err = primary.GetMsg(ctx, 2)118 fmt.Printf(" Primary seq 2: %s\n", status(err))119
120 _, err = mirror.GetMsg(ctx, 2)121 fmt.Printf(" Mirror seq 2: %s\n", status(err))122
123 fmt.Println("\nDone.")124}125
126func status(err error) string {127 if err != nil {128 return "deleted"129 }130 return "still present"131}Mirror streams are your go-to for one-way replication of JetStream data—across regions, accounts, or unreliable networks—without the complexity of full synchronization. Just remember: they’re read-only, deletes don’t replicate in either direction, and they work best with standard retention policies.
And with mirror promotion in NATS 2.12, you’ve got a clean failover path if your origin ever goes down for good.
We’re building the future of NATS at Synadia. Want more content like this? Subscribe to our newsletter
News and content from across the community