Distributed actor framework for Go

Locks, channels, and a broker are three ways to coordinate state. An actor is one.

GoAkt is a distributed actor framework for Go. Typed messages, supervision, clustering, and Grains behind one API, from a single process to a cluster of nodes.

go get github.com/tochemey/goakt/v4

In production at Baki Money and Event Processor. Read a production report ↓

Three GoAkt nodes exchanging messages over every edge node-1 node-2 node-3
TELL / ASK, SAME API ON EVERY EDGE

Your first actor

One struct, one Receive method, one message at a time.

This is the whole program from the quickstart. Nothing is elided.

main.go
package main

import (
    "context"
    "os"
    "os/signal"
    "syscall"
    "time"

    goakt "github.com/tochemey/goakt/v4/actor"
    "github.com/tochemey/goakt/v4/log"
)

// Greet asks the actor to greet someone; Greeting is its reply.
type Greet struct{ Name string }

type Greeting struct{ Message string }

// Greeter is the actor. State lives in unexported fields; no locks are
// needed because an actor processes one message at a time.
type Greeter struct {
    greeted int
}

var _ goakt.Actor = (*Greeter)(nil)

func (x *Greeter) PreStart(*goakt.Context) error { return nil }

func (x *Greeter) Receive(ctx *goakt.ReceiveContext) {
    switch msg := ctx.Message().(type) {
    case *Greet:
        x.greeted++
        ctx.Response(&Greeting{Message: "Hello, " + msg.Name + "!"})
    default:
        ctx.Unhandled()
    }
}

func (x *Greeter) PostStop(*goakt.Context) error { return nil }

func main() {
    ctx := context.Background()
    logger := log.DefaultLogger

    system, err := goakt.NewActorSystem("quickstart", goakt.WithLogger(logger))
    if err != nil {
        logger.Fatal(err)
    }

    if err := system.Start(ctx); err != nil {
        logger.Fatal(err)
    }

    pid, err := system.Spawn(ctx, "greeter", &Greeter{})
    if err != nil {
        logger.Fatal(err)
    }

    response, err := goakt.Ask(ctx, pid, &Greet{Name: "World"}, time.Second)
    if err != nil {
        logger.Fatal(err)
    }

    logger.Info(response.(*Greeting).Message)

    sig := make(chan os.Signal, 1)
    signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
    <-sig

    _ = system.Stop(ctx)
}

Why actors

Four things you stop writing yourself.

Simpler concurrency

Actors process one message at a time. You write plain Go with no locks, channel plumbing, or shared-state bugs.

Location transparency

Send a message to a local, remote, or clustered actor with the same API. The framework handles the wire.

Resilience by design

Supervision trees and the let-it-crash model keep failures contained and recoverable.

Batteries included

Clustering, Grains, CRDTs, streams, scheduling, passivation, and observability, without re-rolling them yourself.

In production

31 pods, one monolith, and no broker in between.

One Go application, deliberately kept a monolith, running on Kubernetes at roughly 31 pods in steady state, with about 16 new pods joining the existing cluster simultaneously during a rolling update. GoAkt is the entire distribution layer: no MQTT or AMQP broker, no service mesh, no external actor registry. The only stateful things next to the app are a database and Redis.

StringKe, production report on a self-hosted analytics platform, July 2026

Also running GoAkt

  • Baki MoneyAI-powered expense tracking that turns receipts into stories.
  • Event ProcessorClustered complex event processing for IoT data streams.

Running GoAkt in production? Write a short report to be listed here, or leave a note in the feedback thread.

Where it does not fit

Three things GoAkt does not try to be.

  • Cluster nodes are Go processes.

    Other languages reach a GoAkt system through the API you expose, not by joining the cluster.

  • Streams are in-memory.

    They replace a broker for fan-out inside a process. They are not a log with retention or replay.

  • Actor state lives in memory.

    Event sourcing, durable state, projections, and sagas are the job of eGo, built on GoAkt.

Numbers

Every figure reproduces from the repository.

12.6Mmsg/s
One actor, one sender
Contention-free dispatch: the per-message cost of Tell.
26.4Mmsg/s
Eight actor pairs in parallel
Aggregate throughput, one private receiver per sender.
1.23KB / actor
One million live actors
Resident memory per actor under sustained load.
Run
2026-09-05
Commit
e3fd47f5
Hardware
Apple M1, 8 threads
Go
1.27.1

Throughput figures are the median of ten runs. Memory is resident heap per actor after spawning one million actors. Commands and methodology are in benchmark/doc.md.

Start

Paste the program above into main.go. Then three commands.

$ go mod init quickstart
$ go get github.com/tochemey/goakt/v4
$ go run .