Clause
← All documentation

Processes

Executor

One runtime runs its processes on its scheduler workers (workers, execution model). Startup makes the entry function's call the first process, the main process, and runs the executor until it ends:

Workers

The executor runs processes on RuntimeOptions::schedulers worker threads: the thread that started the program and one more thread per further worker. Programs take the count from --schedulers N (1 to 1,024, runtime options); the default is one worker per logical processor the program may run on (its affinity mask, as OTP's online schedulers), or per CPU of --scheduler-cpus.

Wakeups and shutdown need no further mechanism, because every change of a process's scheduling state happens under the executor mutex:

CPU binding

Workers are unbound by default: the OS places them on any CPU of the program's affinity mask, as OTP's default +sbt u. --scheduler-cpus SET (runtime options) binds worker N to the N-th CPU of the set, round-robin when there are more workers, like OTP's +sbt/+sct; the timer and I/O threads stay unbound.

Receive timeouts

Timers live in a hierarchical hashed timer wheel, as ERTS keeps them (scheduler/timer_wheel):

Exits

A process other than the main one ends with an exit reason, as in OTP (detail::exit_reason, process/exits). Exit signals carry it to linked processes (links) and 'DOWN' messages to monitoring ones (monitors); error reports show it.

How the process endsExit reasonError report
Its first call returnsnormalNone
exit(Reason) (also normal, kill)ReasonNone
An error (error/1,2,3, badarith, {badmatch, V}, undef, ...){Reason, Stack}Yes
An uncaught throw(Value){{nocatch, Value}, Stack}Yes
An exit signal ends it (exit signals)The signal's reason, killed for exit(Pid, kill)None

An error report is written on stderr when the process ends, after flushing standard output, in the format of OTP's default logger handler:

=ERROR REPORT==== 8-Oct-2026::03:42:15.983000 ===
Error in process <0.8.0> with exit value:
{boom,[{crash_reports,'-main/1-fun-6-',0,[]}]}

The header has the local time; the reason is laid out as ~p does. The main process does not write one: its uncaught exception is the program's (executables).

Messages

Dest ! Msg and erlang:send(Dest, Msg) evaluate Dest first and return Msg.

DestEffect
A pid of a live processMsg is copied flat into the receiver's heap (copying between heaps), as OTP copies messages, a large one into a block the receiver takes over (message hand-off), and appended to its signal inbox; to a receiver running on another worker, into a buffer among its arrivals that it takes when it next looks (arrival buffers)
A pid of a process that has endedNothing; the send succeeds
A registered nameAs for its pid; badarg when no live process has the name
{Name, nonode@nohost} of two atomsAs for the pid registered as Name; nothing when there is none
{Name, Node} for any other nodeNothing (there are no other nodes)
A referenceAs for the owner of an active alias; nothing otherwise
Anything elsebadarg

Arrival buffers

A send to a process running on another worker takes no lock at all (process creation and teardown):

Receive

receive selects among its clauses like case, over the messages of the mailbox:

A link connects two processes both ways (Signals in each context: the linked pids in link order and the trap_exit flag).

Monitors

A monitor is one-way: the monitoring process holds it (by reference, in Signals) and the monitored process keeps the reference and the monitoring pid, so it can send the message when it ends.

Registered names

The executor keeps one table of names (atoms) to pids, and each process its own name (Signals::name).

Process dictionary

put/2, get/0,1, get_keys/0,1 and erase/0,1 behave as OTP's: put/2 and erase/1 return the previous value or undefined, erase/0 returns every {Key, Value} and empties the dictionary, keys match with exact equality (1 and 1.0 are two keys).

Timers

start_timer/3,4 and send_after/3,4 (builtins/erlang/timers.cpp, scheduler/timers.cpp) arm a timer in the executor's wheel (the one receive timeouts use) that sends Msg, or {timeout, TimerRef, Msg}, to a pid or registered name when it fires.

Introspection

process_info/1,2 (builtins/erlang/introspection.cpp, scheduler/introspection.cpp) reads another process under the executor's lock and copies its terms (messages, dictionary) into the caller's heap; a process running on another worker is read once its slice ends. A pid of no live process gives undefined.

Spawn options

Sources: builtins/erlang/spawning.cpp, scheduler/aliases.cpp.

Aliases

An alias is a reference messages can be sent to (Alias ! Msg): alias/0,1 (explicit_unalias, or reply for one message), or a monitor made with monitor/3's {alias, explicit_unalias | demonitor | reply_demonitor} (demonitor ends with the monitor, reply_demonitor also ends the monitor on its first message). unalias/1 ends an alias of the caller. A send to a reference that is no active alias, or whose owner ended, is dropped. A reply alias is consumed only when its message is delivered, after any wait for a busy owner. monitor/3's {tag, Tag} replaces 'DOWN' in the monitor message; port monitors take no options.

Nodes

A Clause program is one node that is not distributed, nonode@nohost, and answers as such an OTP node (builtins/erlang/node.cpp): nodes/0 is [], nodes(this | known) [nonode@nohost], other types []; nodes/2 gives {nonode@nohost, #{connection_id => undefined, node_type => this}} for the keys asked. is_alive/0 is false, monitor_node/2,3 raise error:notalive and disconnect_node/1 returns ignored.

Exit signals

Every exit signal comes from a running process (exit/2, exit_signal/2, link/1) or from the end of a process. The executor acts on it at once when its target does not run on another worker, else once the target's slice ended (workers, scheduler/signals): an ended target leaves the run queue or its wait and is finished (its links signalled, its error report written, its context released) before the sending builtin returns. A long linked chain ends process by process without recursion.

Signal at a processNot trapping exitsTrapping exits
exit(Pid, kill), exit_signal(Pid, kill)Ends with reason killedEnds with reason killed
Reason normal (from a link, or sent to another process)Nothing{'EXIT', From, normal} message
exit(self(), normal)Ends with reason normal (OTP's quirk){'EXIT', Self, normal} message
exit_signal(self(), normal)Nothing{'EXIT', Self, normal} message
Any other reason, including kill from a linkEnds with that reason{'EXIT', From, Reason} message

Ports

Ports are specified in ports: a port is linked to its opener, takes part in links, monitors, exit signals and registered names like a process, and talks to its connected process with messages.

Suspending processes

erlang:suspend_process/1,2 and erlang:resume_process/1 (scheduler/suspension.cpp): each suspend is one hold of the target in the executor, so a suspended process runs again only when every suspend of every suspender is resumed; messages and signals still reach it (an exit signal still ends it), and process_info(Pid, status) reports suspended. The end of a suspender resumes its suspends. A synchronous suspend of a process running on another worker waits for its time slice to end; asynchronous returns at once, {asynchronous, Tag} also sends the caller {Tag, suspended}, and unless_suspending answers false when the caller already suspends the process. Suspending or resuming the caller itself, a process that has ended, a non-pid or bad options are badarg, as is resuming a process the caller does not suspend.

Builtins

BuiltinBehavior
spawn(Fun)badarg unless Fun is a fun; otherwise a new process calls Fun(), raising {badarity, {Fun, []}} in that process for another arity
spawn(M, F, Args)badarg unless M and F are atoms and Args a proper list; otherwise a new process calls M:F(Args...), raising undef in that process when no module exports it and no builtin has that name
spawn_link(Fun), spawn_link(M, F, Args)As spawn, and the new process is linked to the caller (links)
is_process_alive(Pid)badarg unless Pid is a pid; true while its process has not ended
spawn_monitor(Fun), spawn_monitor(M, F, Args)As spawn, returning {Pid, Ref} of a new monitor
link(Pid), unlink(Pid)badarg unless Pid is a pid (links)
monitor(process, Item)badarg for another type or item; a reference (monitors); Item is a pid or a registered name
register(Name, Pid), unregister(Name), whereis(Name), registered()See registered names; Name must be an atom
demonitor(Ref), demonitor(Ref, Options)badarg unless Ref is a reference and Options a proper list of flush and info
exit(Dest, Reason), exit_signal(Dest, Reason)badarg unless Dest is a pid or reference; true after the exit signal
process_flag(trap_exit, Bool), process_flag(min_heap_size, N), process_flag(save_calls, N)The previous setting; badarg for another flag, a non-boolean or a negative or non-integer size
erlang:send(Dest, Msg), Dest ! MsgMsg, after sending it (messages); send/2 is not auto-imported
spawn(Node, ...), spawn_link(Node, ...), spawn_monitor(Node, ...) (/2, /4)This node (nonode@nohost): as without Node. Another node: OTP's useless pid of a node that is not alive, a process that ends at once with reason noconnection, so a link or monitor reports noconnection; badarg for a non-atom node
erlang:send(Dest, Msg, Options)ok, or noconnect for a message to another node with the noconnect option; options nosuspend, noconnect, priority (an ordinary message)
send_nosuspend(Dest, Msg), send_nosuspend(Dest, Msg, [noconnect])true; false for another node with noconnect
link(Pid, Options), exit(Dest, Reason, Options), exit_signal(Dest, Reason, Options)As without options; Options is [] or [priority] (an ordinary signal)
process_flag(Pid, save_calls, N)The previous setting of another process (0..10000); kept, but Clause saves no calls; other flags badarg
processes_iterator(), processes_next(Iter)Iterate the live processes: {Pid, Iter1} or none; a process spawned during the walk may appear
bump_reductions(N)true for a non-negative integer; Clause counts no reductions
erlang:suspend_process(Pid), erlang:suspend_process(Pid, Options), erlang:resume_process(Pid)See suspending processes
erlang:process_display(Pid, backtrace)true after writing the call stack of the live process Pid on standard error, one Module:Function/Arity per line, the current function first; badarg for another type or a process that has ended

The new process joins the end of its parent's worker queue (workers); spawn returns its pid at once.