Skip to content

Process Management

Spawn, monitor, and communicate with child processes. Implements actor-model patterns with message passing, supervision, and lifecycle management.

The process global is always available — it does not require require() and does not need to be listed in modules:.

Get the current frame ID or process ID:

local frame_id = process.id() -- Call chain identifier
local pid = process.pid() -- Process ID

Send message(s) to a process by PID or registered name:

local ok, err = process.send(destination, topic, ...)
ParameterTypeDescription
destinationstringPID or registered name
topicstringTopic name (cannot start with @)
...anyPayload values

Permission: process.send on target PID

-- Basic spawn
local pid, err = process.spawn(id, host, ...)
-- With monitoring (receive EXIT events)
local pid, err = process.spawn_monitored(id, host, ...)
-- With linking (receive LINK_DOWN on abnormal exit)
local pid, err = process.spawn_linked(id, host, ...)
-- Both linked and monitored
local pid, err = process.spawn_linked_monitored(id, host, ...)
ParameterTypeDescription
idstringProcess source ID (e.g., "app.workers:handler")
hoststringHost ID (e.g., "app:processes")
...anyArguments passed to spawned process

Permissions:

  • process.spawn on process id
  • process.host on host id
  • process.spawn.monitored on process id (for monitored variants)
  • process.spawn.linked on process id (for linked variants)
-- Forcefully terminate a process
local ok, err = process.terminate(destination)
-- Request graceful cancellation with an optional reason
local ok, err = process.cancel(destination, "shutting down")
ParameterTypeDescription
destinationstringPID or registered name
reasonstringOptional reason delivered to the target

Permissions: process.terminate, process.cancel on target PID

Monitor or link to an existing process:

-- Monitoring: receive EXIT events when target exits
local ok, err = process.monitor(destination)
local ok, err = process.unmonitor(destination)
-- Linking: bidirectional, receive LINK_DOWN on abnormal exit
local ok, err = process.link(destination)
local ok, err = process.unlink(destination)

Permissions: process.monitor, process.unmonitor, process.link, process.unlink on target PID

local options = process.get_options()
local ok, err = process.set_options({trap_links = true})
FieldTypeDescription
trap_linksbooleanWhether LINK_DOWN events are delivered to events channel

Get channels for receiving messages and lifecycle events:

local inbox = process.inbox() -- Message objects from @inbox topic
local events = process.events() -- Lifecycle events from @events topic
ConstantDescription
process.event.CANCELCancellation requested
process.event.EXITMonitored process exited
process.event.LINK_DOWNLinked process terminated abnormally
FieldTypeDescription
kindstringEvent type constant
fromstringSource PID
resulttableFor EXIT/LINK_DOWN: a {value, error} record; the process return value is at result.value and any error at result.error
reasonstringFor CANCEL: why the process is being cancelled

Subscribe to custom topics:

local ch = process.listen(topic, options)
process.unlisten(ch)
ParameterTypeDescription
topicstringTopic name (cannot start with @)
options.messagebooleanIf true, receive Message objects; if false, raw payloads

When receiving from inbox or with {message = true}:

local msg = inbox:receive()
msg:topic() -- string: topic name
msg:from() -- string|nil: sender PID
msg:payload() -- Payload: wrapper (call :data() to extract)
msg:payload():data() -- any: actual payload value

Spawn a process, wait for its result, and return:

local result, err = process.exec(id, host, ...)

Permissions: process.exec on process id, process.host on host id

Upgrade the current process to a new definition while preserving PID:

-- Upgrade to new version, passing state
process.upgrade(id, ...)
-- Keep same definition, re-run with new state
process.upgrade(nil, preserved_state)

Create a spawner with custom context for child processes:

local spawner = process.with_context({request_id = "123"})

Permission: process.context on “context”

process.with_options(options) creates a spawner that carries spawn-time options (e.g., a network selector) instead of context values:

local spawner = process.with_options({network = "app:tor_proxy"})
OptionTypeDescription
networkstringRegistry ID of a network.* entry to use for the child’s outbound connections

Permission: process.context on “context”; selecting a network additionally requires network.select on that network ID.

SpawnBuilder is immutable - each method returns a new instance:

spawner:with_context(values) -- Add context values
spawner:with_actor(actor) -- Set security actor
spawner:with_scope(scope) -- Set security scope
spawner:with_name(name) -- Set process name
spawner:with_message(topic, ...) -- Queue message to send after spawn
spawner:with_options(options) -- Merge spawn-time options (e.g. network)

Permission: process.security on “security” for :with_actor() and :with_scope()

spawner:spawn(id, host, ...)
spawner:spawn_monitored(id, host, ...)
spawner:spawn_linked(id, host, ...)
spawner:spawn_linked_monitored(id, host, ...)

Same permissions as module-level spawn functions.

Register a process under a name and reach it by that name instead of its PID. Any function that takes a destination (send, terminate, cancel, monitor, link, …) accepts a registered name in place of a PID.

local ok, err = process.registry.register(name) -- self, local scope
local pid, err = process.registry.lookup(name)
local ok, err = process.registry.unregister(name)

The optional scope argument selects the consistency guarantee of the name. It defaults to LOCAL. The four scopes and their guarantees are described in the Cluster Guide; in short:

ConstantVisibilityGuarantee
process.registry.LOCALthis node onlyInstant, node-local
process.registry.EVENTUALcluster-wideEventually consistent (gossip)
process.registry.CONSISTENTcluster-wideLinearizable singleton (Raft)
process.registry.STRONGcluster-wideConsistent + every live node acknowledges

On a standalone node only LOCAL is meaningful; the cluster scopes require clustering.

local ok, err = process.registry.register(name, pid, scope)
ParameterTypeRequiredDefaultDescription
namestringyesName to register
pidstringnoselfPID to register; defaults to the calling process
scopenumbernoLOCALOne of the scope constants above

Returns true on success, or nil, error on failure. Conflicts (name already registered to a different PID under a cluster scope) return errors.ALREADY_EXISTS. Registering the same name to the same PID is idempotent. A STRONG registration blocks until every live node acknowledges or the reservation deadline expires; on timeout it returns an error.

Registering on behalf of a different PID additionally requires the process.registry.foreign permission on the target PID.

local pid, err = process.registry.lookup(name)

Returns the registered PID string, or nil, error with kind errors.NOT_FOUND when the name is not registered.

local ok, err = process.registry.unregister(name, scope)

scope defaults to LOCAL and must match the scope the name was registered under. For CONSISTENT and STRONG, the owning process is the one allowed to unregister; unregistering a name owned by another PID returns false. Names also release automatically when the owning process exits (and, for cluster scopes, when its node leaves), so explicit unregister is for early release.

Permissions control what a calling process can do. All checks use the caller’s security context (actor) against the target resource.

Policies can allow/deny based on:

  • Actor: The security principal making the request
  • Action: The operation being performed (e.g., process.send)
  • Resource: The target (PID, process id, host id, or name)
  • Attributes: Additional context including pid (caller’s process ID)
PermissionFunctionsResource
process.spawnspawn*()process id
process.spawn.monitoredspawn_monitored(), spawn_linked_monitored()process id
process.spawn.linkedspawn_linked(), spawn_linked_monitored()process id
process.hostspawn*(), exec()host id
process.sendsend()target PID
process.execexec()process id
process.terminateterminate()target PID
process.cancelcancel()target PID
process.monitormonitor()target PID
process.unmonitorunmonitor()target PID
process.linklink()target PID
process.unlinkunlink()target PID
process.contextwith_context()”context”
process.security:with_actor(), :with_scope()”security”
process.registry.registerregistry.register()name
process.registry.unregisterregistry.unregister()name
process.registry.foreignregistry.register()target PID

Cluster name scopes are authorized by scope-suffixed variants of these actions (process.registry.register.eventual, .consistent, .strong, and the matching unregister actions), so a policy can grant local naming separately from cluster-wide naming.

Some operations require multiple permissions:

OperationRequired Permissions
spawn()process.spawn + process.host
spawn_monitored()process.spawn + process.spawn.monitored + process.host
spawn_linked()process.spawn + process.spawn.linked + process.host
spawn_linked_monitored()process.spawn + process.spawn.monitored + process.spawn.linked + process.host
exec()process.exec + process.host
spawn with custom actor/scopespawn permissions + process.security
ConditionKind
No context founderrors.INVALID
Frame context not founderrors.INVALID
Missing required argumentserrors.INVALID
Reserved topic prefix (@)errors.INVALID
Invalid duration formaterrors.INVALID
Name not registerederrors.NOT_FOUND
Permission deniederrors.PERMISSION_DENIED
Name already registerederrors.ALREADY_EXISTS

See Error Handling for working with errors.