Build a stateful service
Serving instances are ephemeral — they forget everything between requests. When you need state that survives and is shared (a counter, a cache, a registry, a pub/sub broker, the "current value" of something), put it in a resident service — "resident" because it resides in the node: one instance that's always up (like a daemon), rather than spawned fresh per use and gone when done. The node boot-spawns and supervises it, and it holds its state in memory while answering callers over messages.
It's the same "export functions" shape as any service — the difference is one line in the manifest (resident = true) and that module/struct scope is now this instance's live, in-memory state for the life of the process.
// components/counter/index.ts — module scope is the state; each export is a call.
let count = 0;
export function bump(by: number): number { count += by; return count; }
export function total(): number { return count; }
export type Counter = typeof import(".");// components/counter/src/lib.rs
#[rusm_rs::service]
pub mod counter {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNT: AtomicU64 = AtomicU64::new(0); // state the loop owns
pub fn bump(by: u64) -> u64 { COUNT.fetch_add(by, Ordering::Relaxed) + by }
pub fn total() -> u64 { COUNT.load(Ordering::Relaxed) }
}// components/counter/main.go
func run() {
count := 0 // closed over by the handlers — this instance's state
svc := rusm.NewService()
svc.Handle("bump", rusm.Fn1(func(by int) (int, error) { count += by; return count, nil }))
svc.Handle("total", rusm.Fn0(func() (int, error) { return count, nil }))
svc.Serve()
}Mark it resident in rusm.toml so the node boot-spawns and supervises it:
[components.counter]
capability = "sandboxed"
resident = true # boot-spawned at startup + supervised (auto-restart on crash)A resident is one instance, processing its mailbox one message at a time — so its state needs no locks. Two things to get right: where the state lives, and how callers reach that one instance.
Where the state lives
- In memory (the module / struct / closure scope above) is this instance's working state. It's fast and lock-free, but it's gone on restart — the supervisor restarts a crashed service with a clean slate, by design.
- Durable
kvis for state that must survive a restart, or be shared with ephemeral serving handlers that can't hold a client. The service composes the node'skvstore. This is what the runnable example apps do: thestoreservice and the HTTPapishare one todo list throughkv, so it stays consistent and durable no matter which instance serves a call.
For state shared across the whole app, reach for kv — it's the simplest correct answer in every language.
How callers reach it
A resident is one instance, so to talk to it a caller needs its pid — not spawn, which would start a fresh instance. The service claims a name in its entry; a caller looks that name up and calls the pid. This is ordinary app code you write — two lines on each side:
// Service: export your functions; claim a name in the entry (the runner runs the loop).
Process.register("counter");
// Caller: reach the running instance by name (or pid) — a typed call, with the reply.
const c = connect<Counter>("counter");
const total = await c.bump(1);// Service entry: claim a name, then run the dispatch loop.
rusm_rs::register("counter");
counter::serve();
// Caller: reach the running instance by pid — a typed call, with the reply.
let c = counter::Client::connect(rusm_rs::whereis("counter").unwrap());
let total = c.bump(1).unwrap();// Service: claim a name before Serve().
rusm.Register("counter")
svc.Serve()
// Caller: look it up and call by pid (Call takes any pid, returns the reply).
pid, _ := rusm.Whereis("counter")
total, _ := rusm.Call[int](pid, "bump", 1)All three languages reach a resident the same way — connect/Client::connect/Call give a typed call to an existing instance by name or pid, distinct from spawn, which starts a fresh one. (connect(name) looks the name up for you; pass a pid to skip the lookup.) Reach for kv instead when the state must survive a restart or be shared with ephemeral instances (serving handlers, workers) that have no resident to call — that's the durable, cross-language answer, and what the example apps use.
What you need to know
- Supervised.
resident = trueputs it under the node's supervisor — a crash restarts it (bounded by restart-intensity), with in-memory state reset. Persist anything that must survive tokv. - One writer, no races. A service handles its mailbox one message at a time, so its in-memory state is a single-threaded island — no mutexes, no data races.
- Ephemeral callers use
kv. Serving handlers (HTTP/WS/SSE) and workers are short-lived; when they need shared or durable state, the robust cross-language answer iskv(the example apps' approach), not in-memory state in another process. See the worker-vs-service table in Run one-off work.