Skip to main content
In clustered mode, nodes must discover each other. GoAkt uses a pluggable discovery provider that implements the discovery.Provider interface. Pass the provider to ClusterConfig.WithDiscovery() when creating the cluster.

Provider interface

Provider interface
  • ID: Provider name.
  • Initialize: One-time setup (e.g., connect to backend).
  • Register: Register this node with the discovery backend.
  • Deregister: Remove this node on shutdown.
  • DiscoverPeers: Return peer addresses as host:port strings. Called when the node boots. See Discovery happens at boot for what an empty list and an error mean.
  • Close: Release resources.

Built-in providers

GoAkt ships eight providers. Each one lives in its own package under discovery/ and is created with that package’s NewDiscovery function, which takes a pointer to the package’s Config. The configuration is validated when the cluster starts, so a missing required field surfaces as a startup error. Every provider returns peer addresses on the discovery port, the port set with WithDiscoveryPort that carries membership gossip. Providers that register the node with a backend take the node’s Host and DiscoveryPort in their configuration, and those values must match what the cluster listens on.
  • Static: Package discovery/static. A fixed list of peer addresses. Use when the topology is known upfront.
  • Kubernetes: Package discovery/kubernetes. Lists pods through the Kubernetes API. Use inside a Kubernetes cluster.
  • NATS: Package discovery/nats. Nodes answer discovery requests over a NATS subject. Use when NATS is already part of the platform.
  • mDNS: Package discovery/mdns. Multicast DNS on the local network. Use for development on one LAN.
  • DNS-SD: Package discovery/dnssd. Resolves a DNS name to the peers’ addresses. Use when DNS already knows the nodes.
  • Consul: Package discovery/consul. Registers the node as a Consul service with a health check.
  • etcd: Package discovery/etcd. Registers the node under a leased key.
  • Self-managed: Package discovery/selfmanaged. UDP broadcast on the LAN with no backend and no peer list. Use for local clusters and tests.

Static

The static provider returns the configured list as is. Nothing is registered anywhere, so the list must contain every node that may be running, including nodes that are not up yet. Each entry is a host:port address on the discovery port.
  • Hosts: Required. The peer addresses. Each entry is validated as a TCP address.
Static

Kubernetes

The Kubernetes provider lists pods through the Kubernetes API, matching the configured labels and status.phase=Running. Pods do not need to be Ready to be discovered: on a cold start no pod is Ready until its actor system has joined the cluster, so readiness cannot gate discovery. Terminating pods and pods without an assigned IP are excluded, and an empty result is treated as a retryable error so that simultaneously starting replicas keep retrying until they see each other. The provider uses the in-cluster configuration, so it only runs inside a pod, and the pod’s service account needs permission to list pods in the namespace:
Role
  • Namespace: Required. The namespace to list pods in.
  • PodLabels: Required. A pod must carry all of these labels to count as a peer.
  • DiscoveryPortName: Required. The name of the container port carrying membership gossip. Peer addresses are built from the pod IP and this port.
  • RemotingPortName and PeersPortName: Deprecated and ignored. Peers learn each other’s remoting and peers ports from the node metadata exchanged once they are members. Existing configurations that set them keep working.
Kubernetes
For production deployments, set WithMinimumPeersQuorum to a majority of the replica count (e.g. 2 for 3 replicas). With the default quorum of 1, a node that cannot reach its peers is allowed to operate as a single-node cluster.

NATS

Each node subscribes to the configured subject and answers discovery requests with its host and discovery port. DiscoverPeers publishes a request on that subject and collects the answers for the configured timeout. On shutdown the node publishes a departure message. All nodes of one cluster must use the same subject.
  • NatsServer: Required. The server address, in the form nats://host:port.
  • NatsSubject: Required. The subject the cluster uses for discovery traffic.
  • Host and DiscoveryPort: Required. The address this node advertises.
  • Timeout: How long DiscoverPeers waits for answers. Default 1s.
  • MaxJoinAttempts: How many times to try connecting to the NATS server before giving up. Default 5.
  • ReconnectWait: The backoff between connection attempts. Default 2s.
NewDiscovery also accepts nats.WithLogger to route the provider’s logs to your logger.
NATS

mDNS

Each node advertises a service instance named after the cluster with a unique suffix, and carries the cluster name in its TXT record. Each discovery call browses for five seconds and keeps the instances whose service, domain, port and cluster name match. It returns one IPv4 address per node, plus one IPv6 address when IPv6 is true. Queries travel over IPv4 multicast, and IPv6 addresses come from the AAAA records nodes include in their answers, so the network needs IPv4 multicast even for IPv6 discovery. Multicast must be allowed on the network, which rules out most cloud networks.
  • ServiceName: Required. The cluster name. All nodes of one cluster share it, and each node adds its own suffix on the wire.
  • Service: Required. The service type, for example _goakt._tcp.
  • Domain: Required. The mDNS domain. local and local. are both accepted.
  • Port: The discovery port this node announces. Peers announcing a different port are ignored.
  • IPv6: Optional. When true, IPv6 addresses are returned alongside IPv4 ones.
NewDiscovery also accepts mdns.WithLogger to route the provider’s log output, including the mDNS library’s own messages, to your logger.
mDNS

DNS-SD

The provider resolves the configured name and returns every address in its A and AAAA records. There is no registration: the DNS records are managed outside GoAkt, for example by a headless Service in Kubernetes, a Docker Compose service name or Consul DNS. The addresses come back without a port, and the membership layer fills in this node’s own discovery port, so every node must listen on the same discovery port.
  • DomainName: Required. The name to resolve.
  • IPv6: Optional. When true, only AAAA records are returned. Otherwise all records are returned.
DNS-SD

Consul

Register creates a Consul service named after the actor system, with the node’s host and discovery port as its address and the system name as a tag. A TCP health check against that address is attached by default. DiscoverPeers queries the health endpoint for services with that name and tag, and excludes this node. When a service has no address of its own, the address of the Consul node hosting it is used.
  • Address: Required. The Consul agent, for example 127.0.0.1:8500.
  • ActorSystemName: Required. Used as the service name and tag, so nodes of different systems do not discover each other.
  • Host and DiscoveryPort: Required. The address registered for this node.
  • Datacenter: Optional. The datacenter to register in and query. Defaults to the agent’s datacenter.
  • Token: Optional. The ACL token for authenticated requests.
  • Timeout: The limit for each Consul request. Default 10s.
  • Context: Optional. The context Consul requests run under. Defaults to a background context.
  • HealthCheck: The TCP check Consul runs against the node. Interval defaults to 10s and Timeout to 3s.
  • QueryOptions: How peers are looked up. OnlyPassing restricts results to healthy services. AllowStale accepts answers from follower agents. WaitTime bounds blocking queries. Near sorts results by distance to a node. Datacenter overrides the datacenter for queries. When the field is nil, WaitTime defaults to 1s and the two flags to false.
Consul

etcd

Register grants a lease of TTL seconds and stores the node’s address under the key <ActorSystemName>/<host:port> bound to that lease. The lease is kept alive for as long as Context lives, so a node that dies stops renewing and its entry expires after TTL. DiscoverPeers lists the keys under the system’s prefix and excludes this node.
  • Endpoints: Required. The etcd endpoints.
  • ActorSystemName: Required. The key prefix, so nodes of different systems do not discover each other.
  • Host and DiscoveryPort: Required. The address registered for this node.
  • TTL: Required. The lease duration in seconds. It bounds how long a dead node stays visible.
  • DialTimeout: Required. The limit for connecting to etcd.
  • Timeout: Required. The limit for each etcd request.
  • Context: Optional. Must outlive the provider: cancelling it stops the keep-alive and removes the node from etcd. Defaults to a background context.
  • TLS, Username, Password: Optional. Client TLS and authentication.
etcd

Self-managed (zero config)

The self-managed provider uses UDP broadcast for peer discovery. No third-party services or peer IPs are required; nodes on the same LAN discover each other automatically. Each node broadcasts its address at every interval and keeps a cache of the peers it has heard from. A peer silent for three intervals is dropped. Broadcast does not cross subnets, so this suits local clusters and tests rather than cloud deployments.
  • ClusterName: Required. Only nodes with the same name discover each other.
  • SelfAddress: Required. The host:port this node advertises, on the discovery port.
  • BroadcastPort: The UDP port for announcements. Default 7947.
  • BroadcastInterval: How often this node announces itself. Default 5s.
  • BroadcastAddress: The target for announcements. Default 255.255.255.255. Set it to 127.0.0.1 to keep discovery on the loopback interface, for example in tests.
Self-managed (zero config)

Configuration

Configuration
The provider is used by the cluster to find peers. Remoting must be enabled (WithRemote) for cross-node communication.

Discovery happens at boot

A node uses its provider when it boots, in this order: it starts listening for membership gossip, calls Register, then calls DiscoverPeers and joins the nodes it returns. From then on membership spreads through gossip between the members, not through the provider. What DiscoverPeers returns decides how the boot continues: What happens after boot depends on WithMinimumPeersQuorum:
  • Quorum of 1 (the default): the node serves as soon as it has booted and does not query the provider again. A node that booted alone stays alone until another node joins it.
  • Quorum of 2 or more: the node serves nothing until the cluster has that many members, and while it is below the quorum it queries the provider again every five seconds. A node that found nobody at boot keeps looking.
Two clusters that formed separately never merge. If two nodes boot at the same moment and each one misses the other in its single discovery read, each forms its own cluster, and every actor name and grain identity can then exist once per cluster. With a quorum of 1 this depends entirely on the provider following the two rules below.

The two rules a provider must follow

Nodes that boot at the same moment end up in one cluster when the provider guarantees both of these:
  1. A node is visible before it reads. Once Register has returned on a node, DiscoverPeers on the other nodes lists it.
  2. Reads are consistent. DiscoverPeers returns every node whose Register returned before the call.
With both rules, two nodes cannot both miss each other: if node A reads before node B is visible, then node B reads after node A became visible, and joins it. A provider breaks rule 1 when it lists a node only after the node is ready to serve, for example by reading the Ready endpoints of a Kubernetes Service while the readiness probe waits for the actor system: no node is listed while it boots, so every node that boots at the same moment forms its own cluster. A provider breaks rule 2 when its backend answers from a view that lags behind the registrations, such as a DNS record that has not propagated yet or a registry read that allows stale results. Among the built-in providers, Static, Kubernetes, etcd and NATS with a single server follow both rules. DNS-SD, mDNS, the self-managed provider and Consul with AllowStale or OnlyPassing depend on how fast their backend propagates a new node. Use a quorum of 2 or more with those when nodes can boot at the same moment.

When the view may be incomplete, return an error

A provider that cannot guarantee both rules must not answer with a list that holds no other node while its view may still be catching up. It returns an error instead, which makes the node retry for up to ten seconds. A node that is really alone, such as the first node of a new cluster or a single-node deployment, then boots after those ten seconds. The wrapper below adds that behavior to any provider:
requirePeers
Wiring
The wrapper covers a view that catches up within ten seconds. It does not help a provider that lists a node only once it is ready: no node becomes ready while it is still retrying, so each one waits ten seconds and then boots alone. Such a provider has to list nodes that are still booting.
The sample in playground/issue-1408 runs these cases with three nodes that boot at the same moment.

Custom provider

Implement discovery.Provider and pass it to WithDiscovery. See Extending GoAkt for the interface and wiring. A custom provider must follow the two rules, or return an error while its view may be incomplete.