Skip to main content

Best Practices Overview

This section is opinionated: it answers how the Nexus authors would actually build a production actor system, not a survey of every option. Each page addresses one specific decision.

PageThe question it answers
When to use actorsShould I model this as an actor or as a stateless request handler?
Pooled connections behind actorsI have more actors than DB connections — how does that not deadlock?
Supervision and let-it-crashHow do I handle failures without scattering try/catch everywhere?
Passivation and memoryWhat stops a million long-tail actors eating my RAM?
Scaling outHow do I go from one process to many threads, then many machines?
ObservabilityWhat do I actually want to log and meter for an actor system?
Testing actorsHow do I write tests that aren't flaky and aren't 200ms each?
Single-writer aggregatesHow do I serialise writes against one entity without locking the DB row?
Ask vs tellWhen does the handler need to wait for a reply?
Message designWhat makes a good actor message, and what's a trap?

The five rules

If you take nothing else from this section, keep these:

1. One writer per entity, always. If two actors can write the same row, you're back to optimistic-lock-and-pray. Route every command for an entity through a single actor — writes become serial by construction.

2. Pool connections; don't pool actors. Actor spawn is cheap. Database connections aren't. When you have more live actors than pool slots, use connection lifecycle hooks so each actor borrows on activation and releases on passivation — no permanent slot pinning.

3. Passivate aggressively. Every entity actor that owns a connection or holds non-trivial state should set a ReceiveTimeout. Idle for 60 seconds? Stop. The next message spawns a fresh actor that reloads from storage. Hot entities stay resident; the long tail evaporates.

4. Let it crash. Don't catch exceptions you can't meaningfully handle. The supervisor restarts the actor with a fresh mailbox. Defensive try/catch around every actor handler defeats the whole point.

5. Ask is sync sugar over tell. Every ask()->await() ties up the caller's fiber or coroutine until either the reply or the timeout. For fan-out reads, prefer Future::all([...]). For fire-and-forget side effects, call tell() — don't wait for an acknowledgement that adds no information.

Anti-patterns to avoid

Anti-patternWhy it bitesWhat to do instead
One global actor that owns every entitySingle mailbox → single thread → no concurrencyOne actor per id via EntityRefFactory
EntityBehavior with withConnectionSource() against a poolPool slot pinned forever; pool exhausts after N actorswithConnectionLifecycle($pool->take(...), $pool->release(...))
Spawning actors per HTTP request without passivationMemory grows unbounded under trafficUse perRequestActor() (dies with the request) or set ReceiveTimeout
try/catch around the entire handlerHides the failure from supervision; corrupt state survivesLet it throw; the supervisor restarts; map exceptions to HTTP status codes
ask(...)->await() from inside an actor's receiveCoroutine starvation in the same pool that processes the replyUse tell() and a callback, or refactor the chain
Hand-written 503 fallbackMisses partial-failure modes the pool already exposesRegister PoolExhaustedToServiceUnavailable once at boot
Returning associative arrays from handlersDrift between read and write shapes; no static check on field namesTyped response DTOs

Next steps

Read the pages in the order they appear in the table above if you're new to Nexus. Jump directly to the page that matches the decision you're facing if you've used the framework before.