Stateful concurrency belongs in actors before containers

Architecture SeedlingPlanted Aug 2026

I put stateful concurrency in actors before I put it in containers. A container is an isolation and packaging boundary; it does not decide who owns a conversation, which update happens first, or what should restart when one session fails. An actor does. For agent runtimes, that distinction is architectural: model each addressable session or agent as an actor, then use containers, microVMs, or WebAssembly underneath according to the blast radius.

The reason is state ownership. An actor gives one logical identity a mailbox and processes its messages serially, turning concurrent calls into an ordered stream of state transitions. I would rather express “resume session,” “tool result arrived,” and “cancel run” as messages to one owner than let several request handlers race over a shared record. The mailbox does not make distributed systems magically consistent, but it makes the contention boundary explicit — and explicit ownership is a better starting point than locks scattered through stateless services.

Actors also separate identity from location. A session can remain addressable while its process moves, sleeps, restarts, or lands on another node. Cluster sharding turns that property into an operating model: derive an entity identifier, map it to a shard, and let the runtime route messages to the current owner. The container can disappear without becoming the identity of the agent. This is the inversion I want: sessions are stable logical entities; compute is replaceable capacity.

That inversion matters when the fleet gets large. Most sessions are cold, a few are hot, and activity arrives in bursts. Keeping one container per session wastes capacity, while packing many sessions into a generic service recreates an ownership problem inside the process. Actor passivation gives me a third option: keep active entities resident, evict idle or low-value ones, persist the state required for recovery, and buffer messages while an entity is reactivated. Mature admission policies can account for recency and frequency rather than treating every recently touched session as equally valuable.

Failure handling belongs at this layer too. A container restart is a blunt response: it kills every entity sharing the process and knows nothing about their dependency structure. Supervision trees let me say whether one failed child should restart alone, whether later dependants must restart with it, or whether a coupled group should fail together. They also force me to cap restart intensity, because an unbounded “let it crash” policy is just a restart storm with better branding. Akka Typed and Erlang/OTP differ in their defaults, which is precisely why I treat restart policy as an explicit design decision rather than folklore.

Containers still have an essential job. They enforce CPU and memory limits, constrain syscalls and networks, package dependencies, and give the cluster scheduler a unit it can place. Stronger substrates such as gVisor or microVMs raise the security boundary for untrusted code. But none of them understands mailbox pressure, session affinity, checkpoint age, semantic cancellation, or whether two agent steps mutate the same state. Asking Kubernetes to solve those problems is asking the substrate to infer application semantics it cannot see.

If every task is short, stateless, idempotent, and independent, I would skip actors and schedule ordinary container jobs; adding mailboxes, sharding, and supervision would create machinery without protecting any durable state. The claim begins when concurrent work has identity and history. At that point, process-level isolation alone is too coarse, while shared mutable state is too dangerous.

My preferred runtime therefore has two schedulers, not one confused scheduler. The actor layer owns logical concurrency: identity, ordering, supervision, activation, passivation, and recovery. The container or microVM layer owns physical resources: placement, quotas, kernel isolation, and node failure. Durable checkpoints sit between them so actors can be rehydrated somewhere else. I do not replace containers with actors; I stop promoting containers into a state model. Stateful concurrency belongs in actors first, with containers underneath doing the narrower job they are actually good at.