Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.goakt.dev/llms.txt

Use this file to discover all available pages before exploring further.

Why a fixed dispatcher pool instead of a goroutine per actor?

Earlier versions ran each active actor on its own drainer goroutine. v4.2.1 replaced that with a fixed worker pool — typically max(GOMAXPROCS, 2) workers — that cooperatively multiplexes the entire actor population, following the Akka / Pekko / Erlang / Orleans pattern. Trade-offs driving the change:
  • Goroutine count decoupled from actor count. Systems with 100k actors no longer allocate 100k goroutine stacks. runtime.NumGoroutine() stabilises at workerCount + O(1).
  • Fairness by construction. A per-turn throughput budget (configurable via WithThroughputBudget, default 32) bounds how long one busy actor can hold a worker before yielding — peers aren’t starved by a hot mailbox.
  • Better CPU cache use. When a worker processes several messages for the same actor back-to-back, the CPU keeps that actor’s mailbox, state, and Receive code in its fast on-chip memory (CPU cache). Rotating to a different actor on every message would force the CPU to reload everything from main memory each time — measurably slower.
  • Control-plane priority preserved. Every actor carries a dedicated system mailbox drained before the user mailbox on every turn, so PoisonPill, supervision, passivation, and termination messages cannot queue behind a user-message backlog.
Every user-visible semantic is unchanged: per-actor FIFO, single-threaded execution, PreStart / PostStop contract, reentrancy, panic recovery, and supervision all hold — the three-state Idle / Scheduled / Processing CAS on each PID enforces the single-consumer invariant the mailbox contract depends on. See Dispatcher Pool for the full design.

Why any instead of proto.Message (v4)?

v4 allows any type as actor messages. Benefits:
  • Flexibility — Plain Go structs, protobuf, or custom types
  • Simplicity — No mandatory .proto definitions for simple cases
  • CBOR — Efficient serialization for arbitrary Go types
ProtoSerializer remains the default for protobuf messages. CBOR and custom serializers extend the set of supported types.

Why a custom TCP frame protocol?

GoAkt uses length-prefixed binary frames over TCP instead of gRPC:
  • Low overhead — No HTTP/2, HPACK, or stream multiplexing
  • Control — Connection pools, compression, buffer pooling tuned for actor traffic
  • Fewer dependencies — Leaner than gRPC

Why Olric for cluster state?

Cluster state (actor/grain placement) needs replication. Olric provides:
  • Embedded — No external database
  • Distributed hash map — Configurable quorum for consistency
  • Memberlist — Same membership layer as the cluster

Why CRDTs in addition to Olric?

Actor and grain registry entries are strongly consistent via Olric quorums. Application data that must stay available under partition and merge without a single writer uses CRDTs in the crdt package, replicated by a dedicated Replicator actor. Deltas fan out over the existing topic bus—separate concern from registry reads and writes. See Distributed data.

Why reactive streams on top of actors?

The stream package composes Source → Flow → Sink graphs that materialize as actors per stage, inheriting supervision, lifecycle, and demand-driven backpressure instead of unbounded queues. Streams are optional; they do not replace Tell/Ask. See Streams.

Why a tree-based actor hierarchy?

Mirrors Erlang/OTP and Akka:
  • Lifecycle ordering — Stopping a parent stops descendants first (depth-first)
  • Scoped supervision — Parent defines failure policy for children
  • Namespacing — Addresses reflect tree path; no name collisions

Why separate Actor and Grain?

  • Actors — Explicit spawn/stop; caller controls lifecycle. Best for services and infrastructure.
  • Grains — Identity-addressed; framework manages activation and passivation. Best for entity-per-identity patterns.
Both share the same runtime; choose based on use case.