Skip to main content
WIP · Open source · PHP 8.5+

Concurrent PHP,
done right.

Type-safe actors, supervision trees, event sourcing, multi-process scaling, pluggable runtimes. Erlang/OTP and Akka patterns — in the PHP you already know.

composer require nexus-actors/nexus
demo.php
// Messages are plain readonly classes
readonly class Increment {}

// Stateful actor: receives messages, returns next state
$counter = Behavior::withState(0, function (
ActorContext $ctx, object $msg, int $count,
): BehaviorWithState {
if ($msg instanceof Increment) {
$ctx->log()->info("count: " . ($count + 1));
return BehaviorWithState::next($count + 1);
}
return BehaviorWithState::same();
});

// Create system, spawn actor, send messages
$system = ActorSystem::create('app', new FiberRuntime());
$ref = $system->spawn(Props::fromBehavior($counter), 'counter');
$ref->tell(new Increment());
$ref->tell(new Increment());
$ref->tell(new Increment());
$system->run();

PHP can handle HTTP.
But what about everything else?

The queue worker trap

You start with a simple Redis queue. Then you need retries. Then error handling. Then state management. Then monitoring. Six months later, you've built half of Erlang/OTP -- poorly -- spread across a dozen worker scripts.

No structure for concurrency

PHP has fibers and coroutines now, but no framework for using them safely. No supervision. No message protocols. No way to reason about concurrent state. Raw concurrency primitives are like raw SQL -- powerful and dangerous.

The language barrier

Teams are told to "just use Go" or "switch to Elixir" for concurrent workloads. But your domain knowledge, your team's expertise, and your existing codebase are all in PHP. You shouldn't have to rewrite everything.

Built on proven principles

The actor model has powered telecom switches, stock exchanges, and social networks for decades. Nexus brings the same patterns to PHP.

01

Concurrent by Design

Each actor is a lightweight unit of computation with its own mailbox and state. No shared memory. No locks. No race conditions. Thousands of actors run concurrently, communicating only through immutable messages.

02

Fault-Tolerant by Default

When something fails -- and it will -- the supervision hierarchy handles it automatically. Parent actors decide whether to restart, stop, or escalate failures. Your system recovers without human intervention.

03

Type-Safe at Every Layer

Every ActorRef carries its message type as a generic parameter. Send the wrong message type and Psalm catches it during analysis -- not at 3 AM in production. The entire API is built around readonly classes and immutable value objects.

See it in action

Real patterns, real code. Every example runs as-is.

Supervision that handles failure for you

Actors fail. Networks drop. Databases time out. Instead of wrapping everything in try/catch, define a supervision strategy once. The parent actor applies it automatically -- restart with backoff, stop permanently, or escalate to the next level. Your business logic stays clean.

supervision.php
// When an actor fails, its parent decides what happens.
// No try/catch. No manual retry logic. Just policy.

$strategy = SupervisionStrategy::exponentialBackoff(
initialBackoff: Duration::millis(100),
maxBackoff: Duration::seconds(30),
maxRetries: 5,
multiplier: 2.0,
decider: static fn (Throwable $e) => match (true) {
$e instanceof TransientError => Directive::Restart,
$e instanceof FatalError => Directive::Stop,
default => Directive::Escalate,
},
);

$props = Props::fromBehavior($behavior)
->withSupervision($strategy)
->withMailbox(MailboxConfig::bounded(10_000));

Class-based actors with dependency injection

Not everything fits in a closure. For complex actors, extend AbstractActor and get lifecycle hooks (onPreStart, onPostStop), constructor injection via PSR-11 containers, and clean separation of concerns. Spawn them from your DI container with a single line.

OrderProcessor.php
// Class-based actors with lifecycle hooks
// and PSR-11 dependency injection.

final class OrderProcessor extends AbstractActor
{
public function __construct(
private readonly PaymentGateway $payments,
private readonly Inventory $inventory,
) {}

public function onPreStart(ActorContext $ctx): void
{
$ctx->log()->info('OrderProcessor started');
}

public function handle(
ActorContext $ctx, object $message,
): Behavior {
if ($message instanceof ProcessOrder) {
$this->payments->charge($message->order);
$this->inventory->reserve($message->items);
$message->replyTo->tell(new OrderConfirmed(
orderId: $message->orderId,
));
}

return Behavior::same();
}
}

// Spawn from your DI container
$ref = $system->spawn(
Props::fromContainer($container, OrderProcessor::class),
'order-processor',
);

One codebase, two runtimes

Develop locally with the Fiber runtime -- zero extensions required, instant startup, built into PHP. Deploy to production with the Swoole runtime -- true coroutines, native channels, 100K+ concurrent actors. Switch runtimes by changing a single constructor. Your actor code stays identical.

runtime.php
// Development: PHP Fibers. Zero extensions needed.
$system = ActorSystem::create('dev', new FiberRuntime());

// Production: Swoole coroutines. 100K+ concurrent actors.
$system = ActorSystem::create('prod', new SwooleRuntime(
new SwooleConfig(
maxCoroutines: 100_000,
enableCoroutineHook: true,
),
));

// Your actor code doesn't change. Not a single line.
$ref = $system->spawn(Props::fromBehavior($behavior), 'worker');
$ref->tell(new ProcessTask($payload));

Stash now, process later

Some actors need to initialize before handling work -- connecting to a database, loading configuration, warming caches. With stashing, incoming messages are buffered transparently. When the actor is ready, unstash replays them in order. No messages lost, no timing hacks.

stashing.php
// Actors can buffer messages during initialization.
// When ready, unstash and process them in order.

$initializing = Behavior::receive(
static function (ActorContext $ctx, object $msg)
use ($ready): Behavior
{
if ($msg instanceof DatabaseReady) {
// Connection established. Process buffered work.
$ctx->unstashAll();
return $ready;
}

// Not ready yet. Buffer this message.
$ctx->stash();
return Behavior::same();
},
);

$ref = $system->spawn(Props::fromBehavior($initializing), 'db-writer');

// These arrive before the database is ready.
// They're stashed, then replayed in order.
$ref->tell(new WriteRecord($data1));
$ref->tell(new WriteRecord($data2));
$ref->tell(new DatabaseReady($connection));

Event sourcing, built into the actor model

Persist events, not state. Every state change is captured as an immutable fact. Actors rebuild their state from event history on startup -- crash recovery is automatic. Snapshot strategies keep recovery fast. Swap between DBAL and Doctrine stores with zero code changes.

event-sourcing.php
// Events are immutable facts. State rebuilds from history.
// Crash? Restart? State recovers automatically.

#[MessageType('account.deposited')]
readonly class Deposited {
public function __construct(public float $amount) {}
}

$account = EventSourcedBehavior::create(
persistenceId: PersistenceId::of('account', 'ACC-001'),
emptyState: new AccountState(balance: 0.0),
commandHandler: function (AccountState $state, ActorContext $ctx, object $cmd)
: Effect
{
if ($cmd instanceof Deposit) {
return Effect::persist(new Deposited($cmd->amount))
->thenReply($cmd->replyTo,
fn ($s) => new Balance($s->balance));
}
return Effect::unhandled();
},
eventHandler: function (AccountState $state, object $event)
: AccountState
{
return match (true) {
$event instanceof Deposited => new AccountState(
balance: $state->balance + $event->amount,
),
default => $state,
};
},
)
->withSnapshotStrategy(SnapshotStrategy::everyN(100))
->withEventStore($eventStore)
->toBehavior();

Scale across all CPU cores

ClusterBootstrap starts a Swoole Process\Pool where each worker runs an independent ActorSystem. A consistent hash ring decides actor placement. Cross-worker messaging uses Unix domain sockets at 255K msgs/sec. Your actor code stays exactly the same -- only the deployment topology changes.

cluster.php
// One machine, all CPU cores. Actors span processes transparently.
// Same ActorRef API -- senders don't know if it's local or remote.

$cluster = ClusterBootstrap::create(
ClusterConfig::withWorkers(16),
)
->onWorkerStart(function (ClusterNode $node): void {
// Each worker runs an independent ActorSystem.
// The hash ring decides which worker owns which actor.
$node->spawn(
Props::fromBehavior($orderBehavior),
'order-processor',
);
})
->run();

// Cross-worker messaging uses Unix domain sockets.
// 255K msgs/sec per worker pair. Zero config.

Everything you need

A complete toolkit for building concurrent PHP systems.

λ

Stateful Actors

Manage state explicitly with Behavior::withState(). State transitions are pure functions. No hidden side effects, no shared globals.

Supervision Strategies

One-for-one, all-for-one, and exponential backoff. Custom decider functions per exception type. Automatic retry with configurable limits.

Message Stashing

Buffer messages during transitional states. Unstash when ready. Messages replay in order. Perfect for initialization sequences.

Scheduled Messages

One-shot and repeating timers with cancellation. Schedule messages to self or other actors. Nanosecond-precision Duration values.

Actor Hierarchies

Actors spawn children. Children have paths like /user/orders/order-123. Watch actors for termination signals. Full lifecycle management.

Dead Letter Office

Messages sent to stopped actors are captured, not silently dropped. Inspect dead letters for debugging. No message goes unaccounted for.

Immutable Everything

Readonly classes, immutable behaviors, value objects everywhere. State is passed, not mutated. Static analysis enforces discipline your team can trust.

Event Sourcing

Persist events, rebuild state from history. Automatic crash recovery. Configurable snapshot strategies and retention policies. DBAL and Doctrine backends.

Durable State

Simpler alternative to event sourcing. Persist the latest state directly. Same actor lifecycle integration, same backend options, less ceremony.

Multi-Process Scaling

Scale across all CPU cores with ClusterBootstrap and Swoole Process\Pool. Consistent hash ring for actor placement. Unix socket transport at 255K msgs/sec.

Deterministic Testing

StepRuntime and VirtualClock let you control time and execution order. Write tests that are fast, deterministic, and free of timing flakiness.

PSR Integration

PSR-11 containers for DI, PSR-3 logging, PSR-14 event dispatching, PSR-20 clocks. Nexus works with your existing stack, not against it.

Four steps to your first actor

No framework magic. No code generation. Just straightforward PHP.

01

Define your messages

Messages are plain readonly classes. No interfaces to implement, no serialization boilerplate. Just data.

readonly class PlaceOrder {
public function __construct(
public string $orderId,
public array $items,
public ActorRef $replyTo,
) {}
}
02

Write a behavior

A behavior is a pure function: it receives a message and returns the next behavior. Stateless or stateful — your choice.

$handler = Behavior::receive(
static function (ActorContext $ctx, object $msg)
: Behavior
{
if ($msg instanceof PlaceOrder) {
// process the order
$msg->replyTo->tell(new OrderPlaced(
$msg->orderId,
));
}
return Behavior::same();
},
);
03

Spawn actors

Actors are spawned from Props — a configuration object that binds a behavior to a mailbox and supervision strategy.

$props = Props::fromBehavior($handler)
->withMailbox(MailboxConfig::bounded(5000))
->withSupervision(
SupervisionStrategy::exponentialBackoff(
initialBackoff: Duration::millis(100),
maxBackoff: Duration::seconds(30),
),
);

$ref = $system->spawn($props, 'order-service');
04

Send messages and run

Tell an actor to do something. The runtime handles scheduling, mailbox delivery, and concurrency. You focus on business logic.

$ref->tell(new PlaceOrder(
orderId: 'ORD-2024-001',
items: ['widget-a', 'widget-b'],
replyTo: $confirmationActor,
));

// Start the event loop
$system->run();

Designed for real work

Nexus is under active development. These are the use cases
we're building towards.

Event-Driven Microservices

Replace sprawling queue consumers with actors that own their state and communicate through typed messages. Each service is an actor — or a tree of actors — with built-in fault recovery.

Real-Time Data Pipelines

Ingest, transform, and route high-volume data streams. Actors process messages concurrently with backpressure-aware mailboxes. No data loss, no manual flow control.

Task Orchestration

Coordinate multi-step workflows where each step may fail independently. Parent actors supervise workers, retry transient failures, and escalate permanent ones — automatically.

IoT and Device Management

One actor per device. Thousands of concurrent connections, each with its own state and lifecycle. Actors start when devices connect and stop cleanly when they disconnect.

Financial Transaction Processing

Process payments, transfers, and settlements with event-sourced actors. Every transaction is an immutable event with full audit trail. Actors maintain account balances without database locks.

Game Servers and Simulations

Model game entities as actors. Players, rooms, NPCs — each with independent state and behavior. Concurrent updates without shared memory or mutex contention.

We didn't invent the actor model.
We brought it home to PHP.

The actor model was conceived in 1973. Erlang proved it could run telecom systems with 99.9999999% uptime. Akka scaled it to millions of concurrent users on the JVM. These aren't experimental ideas — they're battle-tested patterns with decades of production validation.

PHP teams have been locked out of these patterns. Not because the language can't handle it — PHP 8.5 has fibers, readonly classes, enums, and first-class callable syntax. The missing piece was a framework that takes these primitives seriously.

Nexus fills that gap. Every design decision serves a single goal: let PHP developers build concurrent systems with the same confidence that Erlang and Akka developers have enjoyed for years.

Explicit over implicit

State is passed as function arguments, not hidden in object properties. Behaviors are returned, not mutated. You can read any actor handler and understand it completely.

Let it crash

Don't write defensive code against every possible failure. Write actors that handle the happy path. Let the supervision hierarchy handle everything else.

Composition over inheritance

Behaviors compose. Props compose. Supervision strategies compose. Small, focused pieces snap together into complex systems. No deep class hierarchies.

No magic

No annotations that generate code. No runtime reflection. No hidden service locators. Every behavior is a plain function. Every message is a plain class.

Modular by design

Pick what you need. Leave what you don't.

nexus-actors/core

Actors, behaviors, supervision, mailboxes, and the full type-safe API. Runtime-agnostic. Zero dependencies beyond PSR interfaces.

nexus-actors/runtime-fiber

Fiber-based runtime. No extensions. Cooperative multitasking with PHP's native fiber scheduler. Ideal for development and testing.

nexus-actors/runtime-swoole

Swoole coroutine runtime. Native channels, true async I/O, 100K+ concurrent actors. Built for production workloads.

nexus-actors/serialization

Message serialization with envelope protocol. PHP native serializer for speed, Valinor mapper for structured wire formats.

nexus-actors/persistence

Event sourcing and durable state abstractions. Effects, snapshots, retention policies, concurrency control, and in-memory stores for testing.

nexus-actors/persistence-dbal

Doctrine DBAL storage backends. SQL-backed event, snapshot, and durable state stores. Works with SQLite, PostgreSQL, MySQL.

nexus-actors/persistence-doctrine

Doctrine ORM adapter. Entity-based stores using EntityManager. Same table schema as the DBAL package.

nexus-actors/cluster

Pure PHP abstractions for scaling: consistent hash ring, remote actor refs, pluggable transport and directory interfaces.

nexus-actors/cluster-swoole

Swoole multi-process scaling. ClusterBootstrap, Unix socket transport, shared-memory actor directory via Swoole\Table.

nexus-actors/psalm

Psalm plugin for static analysis of actor message protocols. Type providers and rules that catch errors before runtime.

Try it in five minutes.

Install, create demo.php, and run it. That's the whole setup.

composer require nexus-actors/core nexus-actors/runtime-fiber && php demo.php