Blog

How Elixir Processes Handle Messages: Timeouts, Mailboxes, Crashes, and Infinite Loops

One of the most common points of confusion for developers new to Elixir (and the BEAM VM) is how process messaging actually works—especially around timeouts, mailboxes, and whether processes “keep running forever”.

This article clears that confusion step by step.

We’ll cover:

  • How messages are delivered and stored
  • What receive … after really does
  • Whether messages are lost after timeouts
  • When processes run infinitely (and when they don’t)
  • Whether you can read all messages later
  • What happens on crashes and exits
  • How OTP supervisors fit into all this

1. Elixir Process Basics (The Foundation)

An Elixir process is:

  • Extremely lightweight
  • Isolated (no shared memory)
  • Communicates only via message passing
  • Owns a private mailbox

Each process has:

  • A mailbox (queue of messages)
  • A heap (its state)
  • An execution loop (your code)

Messages are sent using:

send(pid, message)

Messages are delivered asynchronously and stored in the receiver’s mailbox.


2. How Messages Are Processed

Messages are processed using receive:

receive do
  msg -> handle(msg)
end

Important rules:

  1. Messages are not pulled automatically
  2. A process must explicitly call receive
  3. receive scans the mailbox from oldest to newest
  4. Only the first matching message is consumed
  5. Unmatched messages stay in the mailbox

3. What Happens When a Timeout Is Used?

Consider this code:

receive do
  msg -> handle(msg)
after
  3_000 ->
    :timeout
end

Key clarification

The timeout only ends the receive block — not the process.

What actually happens:

  • The process waits up to 3 seconds
  • If no matching message arrives:
    • The after clause runs
  • Execution continues to the next line of code

Messages are NOT dropped

If a message arrives after the timeout:

send(pid, :hello)

That message:

  • Goes into the mailbox
  • Is not discarded
  • Can be received later

Timeouts do not affect message delivery.


4. So Why Do Messages Sometimes “Disappear”?

Messages are lost only if the process is no longer alive.

Example:

receive do
  msg -> handle(msg)
after
  3_000 -> :timeout
end
# process exits here

If the process exits:

  • Mailbox is destroyed
  • Late messages are lost

📌 Timeout ≠ process exit
📌 Process exit = mailbox destruction


5. Do Processes Run Infinite Loops?

Only if you write a loop.

No loop → process exits

receive do
  msg -> handle(msg)
end

After one message:

  • Function ends
  • Process exits

Loop → process stays alive

def loop do
  receive do
    msg -> handle(msg)
  after
    3_000 -> :idle
  end

  loop()
end

Now the process:

  • Keeps receiving messages forever
  • Survives timeouts
  • Can receive late messages

📌 Infinite behavior is explicit, not implicit.


6. Can You Access All Messages Later?

Short answer: No

Important BEAM guarantees:

  • Mailboxes are private
  • Messages are destructively read
  • Once consumed, messages are gone forever
  • You cannot inspect another process’s mailbox

This is intentional, to preserve:

  • Isolation
  • Determinism
  • Fault tolerance

What you can do

  • Drain your own mailbox
  • Log messages explicitly
  • Forward messages to an observer
  • Persist events externally (DB, ETS, Kafka, etc.)

If you need replayability, you need an event system, not a mailbox.


7. What If a Message Doesn’t Match Any Pattern?

Example:

receive do
  {:ok, msg} -> handle(msg)
end

If the mailbox contains:

:error
{:ok, 42}

What happens?

  • :error does not match
  • It stays in the mailbox
  • {:ok, 42} is consumed

📌 Unmatched messages stay forever unless handled.

This can cause:

  • Mailbox growth
  • Performance degradation
  • “Mailbox poisoning”

Best practice

Always include a catch-all:

receive do
  known -> handle(known)
  unexpected -> log(unexpected)
end

8. What Happens on Crashes and Exits?

If a process crashes:

  • Mailbox is destroyed
  • State is lost
  • PID becomes invalid
  • Messages sent afterward are lost

This is why Elixir embraces:

Let it crash

Instead of defensive code, Elixir relies on supervision.


9. Where OTP Supervisors Come In

A GenServer or supervised process:

  • Runs an internal infinite loop
  • Is restarted automatically on crash
  • Rebuilds state in init/1
  • Keeps message handling safe

Supervisors:

  • Trap exits
  • Monitor child processes
  • Restart them based on strategy

Restarted processes are brand new — state must be:

  • Reconstructed
  • Loaded from DB
  • Externalized

10. The Correct Mental Model

Think of an Elixir process as:

A worker sitting in a room with a mailbox

  • Messages arrive anytime
  • receive = checking the mailbox
  • Timeout = “I’ll stop checking for now”
  • Loop = staying in the room
  • Exit = room destroyed

Final Takeaways (TL;DR)

  • Timeouts do not drop messages
  • Messages always go to the mailbox if the process is alive
  • Processes do not loop automatically
  • Infinite behavior requires explicit loops or GenServer
  • Unmatched messages stay in the mailbox
  • Mailboxes cannot be inspected or replayed
  • Crashes destroy state and mailbox
  • Supervisors restart clean processes

One-line summary

Elixir processes only receive messages while alive and looping; timeouts stop waiting, not message delivery, and OTP supervision exists to manage crashes safely.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.