Table of Contents

Broadcast with gossip

Join a topic, broadcast to the swarm, and read events — the iroh-gossip pub/sub mesh in Go.

Gossip gives you topic-based broadcast across a mesh of endpoints without a server. Peers join a topic, maintain a partial view of neighbors (HyParView), and relay messages to the swarm (PlumTree), so a message reaches every subscriber without anyone holding a connection to everyone.

The Go package is github.com/tmc/go-iroh/gossip, a port of the Rust iroh-gossip crate speaking /iroh-gossip/1. Upstream’s conceptual treatment is Gossip Broadcast.

Wire it up

Gossip is a protocol behind a Router, like any other:

ep, err := iroh.Bind(ctx)
if err != nil {
	return err
}
g := gossip.NewGossip(ep)
_, err = iroh.NewRouter(ep, map[string]iroh.ProtocolHandler{
	gossip.ALPN: g.Handler(),
}, nil)

gossip.ALPN is the registered protocol name — do not invent your own, or you will not interoperate with Rust peers.

Topics

A topic is a 32-byte ID. Derive it from a name with BLAKE3 so every peer that knows the name computes the same ID:

topic := gossip.TopicID(blake3.Sum256([]byte("example/chat")))

Subscribing needs at least one bootstrap peer to reach an existing swarm — the mesh has to be entered somewhere:

// First peer: nothing to bootstrap from yet.
ta, err := ga.Subscribe(ctx, topic, nil)

// Second peer: bootstrap from the first, and wait until actually joined.
tb, err := gb.SubscribeAndJoin(ctx, topic, []netaddr.EndpointAddr{seed})

Subscribe returns as soon as the subscription exists locally; SubscribeAndJoin additionally waits for the swarm join to complete. Use the latter when the next thing you do is broadcast — otherwise the message can go out before you have a neighbor and reach nobody. Topic.Joined and Topic.IsJoined let you check explicitly.

Broadcast and receive

if err := tb.Broadcast(ctx, []byte("hello topic")); err != nil {
	return err
}

for ev, err := range ta.Events() {
	if err != nil {
		return err
	}
	if ev.Kind == gossip.Received {
		fmt.Printf("received: %s (from %s)\n", ev.Content, ev.DeliveredFrom.Z32()[:8])
		break
	}
}

Running the two-endpoint version of this prints:

B neighbors: 1
A received: hello topic (from bibzrh75)

Events() is an iter.Seq2[Event, error], so it works with range. Event kinds include NeighborUp and NeighborDown alongside Received, which is how you track the mesh as it changes.

DeliveredFrom is the last hop, not the original sender. In a multi-hop mesh the message may have been relayed, so do not use it as an author identity. If you need to know who wrote a message, put the author in your own payload and sign it.

Broadcast reaches the whole swarm; BroadcastNeighbors reaches only your immediate neighbors.

Sizing and splitting

gossip.WithMaxMessageSize(n) sets the cap at construction, readable back with Gossip.MaxMessageSize(). Oversized messages fail to broadcast rather than fragmenting, so keep payloads well under the limit and chunk in your own protocol if you need more.

Topic.Split() returns a *Sender and *Receiver when you want to hand the two halves to different goroutines.

Discovery on top of gossip

The package also ships gossip.Discovery, which uses a gossip topic as a peer discovery channel — gossip.DefaultDiscoveryTopic is the well-known ID. It implements the same publisher/resolver interfaces as the other discovery services, so it registers with iroh.AddressLookupServices like DNS or mDNS. See Relays and discovery.

Next steps

Last updated: 2026-08-20