Skip to content

FAQ

The framework is pre-1.0. The API surface is stable enough that we use it in real applications, but expect breaking changes until 1.0 ships. See Version policy.

What that means in practice:

  • The core actor model (Actor, mailboxes, dispatchers, supervision, persistence basics) is well-tested and stable.
  • Cluster features (sharding, singleton, distributed data) have working implementations but expect API polish.
  • Some edges are explicitly experimental — replicated event sourcing, sharded daemon process variants, etc. Pages flag these.

If you’re starting a new project where you’d ship before 1.0, it’s a reasonable bet. If you’re rewriting a critical system, wait or budget time for breaking-change adoption.

Why “actor-ts”? Why not just use Akka.NET or vanilla TS?

Section titled “Why “actor-ts”? Why not just use Akka.NET or vanilla TS?”

Three reasons:

  • First-class TypeScript types. Akka.NET assumes C#; an Akka-style port to TS often loses the type-level benefits. actor-ts is designed for TypeScript-first ergonomics.
  • Runtime choice. Runs on Bun (primary), Node (≥22.12), and Deno — no JVM, no .NET, no separate runtime.
  • Cluster + persistence + observability built in, without the “wire it yourself from 8 libraries” tax that vanilla TS would charge.

If you don’t need clustering / persistence / supervision, vanilla Promise-based code is simpler and you should use it. Reach for actor-ts when the runtime-model benefits actually matter.

No. The framework targets server-side runtimes — Bun, Node, Deno — because most actor patterns (clustering, persistence, TCP transports, file-based journals) need server-side primitives. A subset could in theory run in the browser, but there’s no browser-mode build.

For browser-side reactive patterns, look at signals/effects libraries (Solid, MobX) — different model, better fit.

Both work; both run on the same engine. Pick:

  • Untyped if you’re learning the actor model, want one-to-one parity with Akka examples, or have an actor with substantial mutable state.
  • Typed if you want functional state-machine ergonomics, cast-free child spawning, or are coming from Akka-Typed / Cats Effect.

You can mix them — typed parents spawn untyped children and vice versa. See Typed overview.

  • tell is the default. Fire-and-forget. Use unless you specifically need a reply.
  • ask when you need a Promise<Reply> — typically HTTP handlers, sagas, tests. Has overhead (temporary ref) so don’t use it in tight loops.
  • pipeTo when an async operation should land as a message in an actor’s mailbox. The non-blocking alternative to await inside onReceive.

See Ask pattern and Future patterns.

Pick by whether you need history:

  • PersistentActor persists every state-changing event. Replay to recover. Use when history matters (audit, time travel, projections, append-only domains).
  • DurableStateActor persists the current state snapshot, overwriting on each update. Use when the current value is all you need.

See Persistence overview.

  • Singleton — exactly one actor cluster-wide. Coordinators, schedulers, leader-elected services.
  • Sharding — one actor per key, distributed across nodes. Per-user sessions, per-IoT-device controllers.
  • DistributedData — eventually-consistent shared state via CRDTs. Counters, flags, sets that everyone reads and writes.

See the Distribution overviews.

In rough numbers (single Bun process, 4-core machine):

  • 100 000 actors is comfortable — minimal memory overhead per actor, sub-millisecond send latency.
  • 1 000 000 actors is feasible but tight on memory. Consider passivation patterns (idle entities stop themselves).
  • 10 000 000+ actors needs sharding across nodes — one node can’t hold that many efficiently.

The dominant cost is the actor’s own state, not the framework’s bookkeeping.

Roughly:

  • tell — ~50 ns per call on hot path (microtask dispatcher). Memory: ~200 bytes per envelope.
  • ask — ~5 μs per call (temporary ref construction). Memory: ~500 bytes plus Promise machinery.
  • Cross-cluster tell — bounded by network + serialization cost. Sub-millisecond on localhost, ms-range over LAN.

For comparison: a raw function call is ~1 ns. Actor messaging costs 50-200× a direct call — which is fine for most workloads but visible if you’d hammer the same actor with millions of messages per second.

The framework doesn’t allocate aggressively, but it does allocate. Hot paths (tell, mailbox enqueue/dequeue) create envelopes per message; high-throughput actors can show in GC profiles.

For low-allocation hot paths:

  • Reuse message objects when safe (but only if the receiver doesn’t keep a reference).
  • Use MicrotaskDispatcher for compute-heavy actors that don’t share the event loop with HTTP I/O.

One node is valid. Cluster.join with no seeds (or unreachable seeds) gives you a singleton cluster — the local node auto-promotes to leader. Every cluster extension (sharding, singleton, pubsub) works in single-node mode.

This means you can develop and test cluster code without a Docker Compose setup.

Two layers:

Without a downing strategy, the default behavior is “wait indefinitely” — operators must down nodes manually. Pick one for production.

Can I use Kubernetes for cluster discovery?

Section titled “Can I use Kubernetes for cluster discovery?”

Yes — the kubernetes-api seed provider lists pods matching a label selector and uses them as seeds. See Discovery — Kubernetes API.

For lease-based singletons in K8s, the Kubernetes lease implementation uses K8s native Lease CRDs.

  • InMemoryJournal for tests and dev.
  • SqliteJournal for single-node production (best performance-to-simplicity ratio).
  • CassandraJournal when multiple nodes need to share the same event stream.
  • Custom when none of these fit — implement the Journal interface against your storage.

See Journals — In-memory and SQLite.

Use event adapters. Every event persists in a versioned envelope ({ _v, _t, _e }); the adapter’s upcast(stored, version) is called on read to migrate older versions to the current shape.

See Migration overview.

Each projection persists its own offset; if a projection crashes after processing event N but before persisting offset N, it’ll re-process N on restart. In that sense, at-least-once.

For exactly-once semantics, the projection must be idempotent (use the event’s seqNr as a dedup key) or transactional (commit the view + offset atomically). See Projections.

We test against TypeScript 5.6+. Earlier versions may work but aren’t supported. The framework uses some features (e.g., const type parameters) that landed in recent releases.

It’s the cleanest way we found to express match().exhaustive() in TS — the discriminated-union dispatch pattern is everywhere in the codebase and a hand-rolled switch loses the exhaustiveness check.

You’re not required to use ts-pattern in your own code; plain if/else ladders work fine. See Pattern matching.

Bun if you can. Faster startup, faster cold paths, native SQLite, native test runner. We test Bun as the primary runtime.

Node (≥22.12) is fully supported as a fallback. Some performance is left on the table compared to Bun, but the feature surface is identical.

Deno is supported but receives less testing — file a bug if you hit a Deno-specific issue.

See Runtime — Compatibility matrix.

Spiritually yes — the actor model, supervision, sharding, persistence, distributed data, the cluster gossip protocol all trace their lineage to Akka.

Differences:

  • No Scala / JVM — pure TypeScript, runs on JS runtimes.
  • No fiber-based concurrency — JavaScript is single-threaded per process; we use the microtask/macrotask queue.
  • No streaming — Akka Streams isn’t ported. Use a separate library for backpressure-driven async pipelines.
  • Smaller surface — only the actor-model parts. Akka HTTP, Akka Persistence Query, Alpakka, etc. have analogs but are intentionally simpler than the JVM counterparts.

If you need Akka exactly, use Akka. This is a TypeScript-first port that picks the most-useful 80%.

Orleans actors are virtual — addressed by ID, auto-spawned on first message, auto-passivated when idle. actor-ts sharding is the closest analog (see Sharding overview). Differences:

  • Sharding entities are not virtual by default — they’re passivated explicitly (or by idle timeout).
  • Sharded entity placement is one-actor-per-key, like Orleans grains, but the placement strategy is hash-mod-region by default, not Orleans’s directory-based.

Migration guide: from-orleans.