← all diagrams · when one side fails · React · Temporal · ECS
Async and memory-safe: why GenServers suit short-lived agents
mycelia's agents come and go all day: one wakes, streams for an hour, sleeps after 30 idle minutes, gets restarted after a crash. On the BEAM, each one is a set of tiny processes with their own memory, that only talk by sending messages. This page shows what that buys, next to the usual alternative: one server process whose agents all share one memory and one event loop.
Try it: five agents, two runtimes
Both sides run the same five agents. The counters show each agent getting work done. Pick something to happen to spore 3 (◀) and watch the other four.
BEAM: a process per agent
One shared process
A simplified model to show the shape of each runtime, not a benchmark. A carefully written single-process server can catch many of these (try/except around each task, worker threads, memory caps); the difference is that the BEAM gives you the isolation by default, for every process.
"Async": every process waits on its own
On the BEAM
- Sending a message never blocks. It lands in the other process's mailbox; the sender carries on.
GenServer.castis exactly that. GenServer.callwaits, but only the caller waits, with a timeout (5 s by default). Nothing else in the system is held up.- No async/await. Code is written straight down, and can simply wait (
receive, a slow HTTP read). The VM parks that one process and runs others. - Preemptive scheduling. Each process gets a small budget of work (about 4,000 "reductions"), then the scheduler switches. A process stuck in a loop slows itself, not its neighbours.
In one event loop
- Concurrency is cooperative: tasks take turns only at an
await. - A task that computes for 3 seconds without awaiting stops every other task for 3 seconds.
- Every function that might wait has to be
async, and so does everything that calls it. - Fine for I/O-heavy work, if every library behaves.
In mycelia: each spore's tree has a Task that reads the pod's event stream, separate from the Agent GenServer. The reader can sit on a slow stream for an hour while the Agent stays free to answer the page. Six spores streaming at once are six processes waiting, which costs almost nothing.
"Memory-safe": nothing is shared, so nothing can be corrupted
On the BEAM
- Each process has its own heap. A process starts with a few hundred words of memory and grows as it needs.
- Data is immutable, and messages are copied into the receiver's heap. No other process can change your data under you: no locks, no data races.
- Garbage collection is per process. Collecting one agent's heap pauses only that agent, briefly; there's no stop-the-world pause for everyone.
- When a process dies, all its memory is freed at once. No reference from elsewhere can keep it alive, because nothing else could point into it.
- A per-process cap is possible:
max_heap_sizekills a process that grows too big, and its supervisor restarts it, before it hurts anyone else.
With one shared heap
- All agents' objects live together; any code can hold a reference to any of them.
- One agent's huge allocation means a bigger collection, which pauses all of them.
- A finished agent's data is only freed if nothing still points to it: a forgotten cache entry or listener is a leak that grows with every agent that comes and goes.
- If the process runs out of memory, every agent in it goes down together.
max_heap_size isn't something these diagrams say.Why this fits ephemeral agents
| In an agent's life | What the BEAM does | Why it matters |
|---|---|---|
| it wakes | starts a tree of processes in microseconds, with a few KB each | Waking is cheap, so sleeping after 30 idle minutes costs nothing to undo. Up to 16 spores' trees share one mycelia pod. |
| it streams for an hour | the reader process waits on the stream; the scheduler runs everyone else | Long, slow, mostly idle work is the normal case, not a problem to engineer around. |
| it produces a huge output | only its own heap grows and is collected | One chatty agent doesn't make the page stutter for everyone else. |
| it hits a bug | its process dies; its supervisor restarts it in a clean state | A crash is a restart of one agent, not an outage. See when one side fails. |
| it goes to sleep | its processes exit; all their memory is freed instantly | Thousands of wake/sleep cycles leave nothing behind. A server that runs for months doesn't slowly fill up with dead agents' data. |
The same idea, one level down
The BEAM isolates each agent's processes inside mycelia. Kubernetes does the same for the heavy part, each agent's pod: its own memory (3 GiB, enforced), its own CPU (2), its own files. When a pod runs out of memory, Kubernetes kills that pod alone, and the Host starts a new one. So an agent is isolated twice: its small bookkeeping on the BEAM, and its real work in a pod.