Erlang’s Preemptive Scheduling: Keeping Your Heartbeat Alive Amid CPU‑Intensive Work
Learn how Erlang’s preemptive scheduling via reductions keeps a heartbeat process alive while a CPU‑intensive loop runs. A practical example shows the scheduler’s fairness and highlights key trade‑offs.
04 Dec 2025, 16:55 UTC

Concrete Problem: A Heartbeat That Gets Stuck
When you build a real‑time service, you often need a small process that sends a “heartbeat” message every second to a monitoring system. In many languages, a long‑running CPU task can block the thread that would otherwise send the heartbeat, causing the entire service to appear dead.
In Erlang, the BEAM VM’s scheduler guarantees that no single process can monopolise a CPU core. This means a CPU‑intensive loop will not starve a heartbeat process, as long as you use the language’s process model correctly.
Why Erlang Uses Reductions for Preemption
The BEAM scheduler implements preemptive multitasking based on “reductions”. A reduction is the execution of a bytecode instruction. The scheduler counts reductions per process and, after a configurable threshold (default 5000), it preempts the process and switches to another one.
Because Erlang processes are lightweight (they are managed by the VM, not by the OS), the cost of context switching is tiny. The VM automatically distributes processes across all logical cores via multiple schedulers, so each core runs a mix of short and long processes.
Worked Example: Heartbeat vs. Tight Loop
Below is a minimal example that demonstrates how a heartbeat process stays responsive even when another process is busy with a tight loop.
%% Start a heartbeat that sends a message every 1 second.
heartbeat() ->
spawn(fun heartbeat_loop/0).
heartbeat_loop() ->
timer:sleep(1000),
io:format("~p: Heartbeat~n", [self()]),
heartbeat_loop().
%% CPU‑intensive process: a tight loop that counts to a large number.
busy_loop() ->
spawn(fun() -> count(1_000_000_000) end).
count(N) when N > 0 ->
count(N - 1);
count(0) -> ok.
Run the example in an Erlang shell:
1> c(heartbeat). % compile the module
2> H = heartbeat:heartbeat().
3> B = heartbeat:busy_loop().
4> timer:sleep(5000).
During the 5‑second sleep, you should see several “Heartbeat” messages printed, even though the busy loop is actively decrementing a counter. The BEAM scheduler preempts the busy loop after its reduction quota is reached, allowing the heartbeat to run.
Checking Reduction Counts
To confirm that preemption is happening, inspect the reduction count of the busy process:
5> erlang:process_info(B, reductions).
{reductions, 5000}
The count resets after the scheduler switches to another process. If you run the busy loop for a very long time, you will see the reductions count periodically reset to a new value.
Trade‑Offs and Limitations
- CPU‑intensive loops can still cause latency – If a loop is written in a way that performs a large amount of work per reduction (e.g., a tight loop that does heavy native calls), it may exhaust the reduction quota quickly and still starve other processes.
- Unbounded message queues – Processes that cannot keep up with incoming messages will accumulate them in memory, potentially exhausting the node’s RAM. Use bounded queues or back‑pressure patterns.
- No shared state – Because processes never share memory, you must design your system around message passing and immutable data. This can be a paradigm shift for developers coming from Java or C++.
Actionable Takeaway
When designing high‑concurrency Erlang systems:
- Use
spawn/1(orspawn_link/1for supervision) to create lightweight workers. - Keep loops short and avoid blocking calls; rely on the scheduler’s reduction limits to preempt long tasks.
- Periodically inspect
erlang:process_info(Pid, reductions)during development to detect potential starvation. - Implement supervision trees so that a crashing worker does not bring down the entire service.
By embracing Erlang’s preemptive scheduling and process isolation, you can build systems that remain responsive even under heavy computational load.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.