Prev Next

Erlang / Erlang Basics Interview questions

1. What is Erlang? 2. What is the BEAM virtual machine? 3. What are processes in Erlang? 4. What is the actor model in Erlang? 5. What are atoms in Erlang? 6. What are tuples in Erlang? 7. What are lists in Erlang? 8. What is pattern matching in Erlang? 9. Define immutability in Erlang? 10. What is a PID? 11. What is a module in Erlang? 12. What are guards used for in Erlang? 13. What is OTP? 14. What is a gen_server? 15. What is a supervisor? 16. Describe the "let it crash" philosophy? 17. What are records in Erlang? 18. What is message passing? 19. What is hot code loading? 20. What are binaries/bitstrings in Erlang? 21. What is ETS? 22. What is Mnesia? 23. What are list comprehensions? 24. What is a behavior in Erlang? 25. What are ports in Erlang? 26. What is a NIF (Native Implemented Function)? 27. How do you spawn a process in Erlang? 28. How does Erlang achieve concurrency without shared memory? 29. What is the difference between spawn and spawn_link? 30. What is the difference between a list and a tuple? 31. Why is Erlang considered fault-tolerant? 32. How does pattern matching differ from equality comparison? 33. When should you use ETS instead of process state? 34. How do you handle errors in Erlang without try/catch? 35. Why doesn't Erlang have mutable variables? 36. What is the difference between gen_server and gen_statem? 37. How do you implement a simple supervision tree? 38. What happens when a linked process crashes? 39. When would you choose Mnesia over ETS? 40. How do you use the observer tool to inspect a running system? 41. Explain the execution flow of a gen_server call? 42. Explain the internal working of the BEAM scheduler? 43. How can you optimize message passing between heavy processes? 44. Explain the lifecycle of a supervised process? 45. How do you troubleshoot process mailbox overflow? 46. What is the difference between synchronous and asynchronous message passing in OTP? 47. Which is better for state management, ETS or process dictionaries, and why? 48. How does distributed Erlang handle node failures?
Could not find what you were looking for? send us the question and we would be happy to answer your question.

1. What is Erlang?

Erlang is a functional, concurrent programming language built by Ericsson in the late 1980s to run telecom switches that had to stay up for years without a reboot. It compiles to bytecode that runs on its own virtual machine, the BEAM, rather than the OS running Erlang code natively.

The language pairs immutable data and pattern matching with a lightweight process model, so a single Erlang node can juggle hundreds of thousands of concurrent, isolated units of work. Because processes share nothing and talk only through messages, a bug in one process rarely takes down the rest of the system.

Today it powers systems where uptime matters more than raw throughput — messaging platforms like WhatsApp, telecom signaling, and financial trading gateways are common examples.

Erlang was originally built by Ericsson for which purpose?
Erlang code executes on which of the following?

2. What is the BEAM virtual machine?

The BEAM (Bjorn's Erlang Abstract Machine) is the runtime that executes compiled Erlang bytecode. It is not a general JIT like the JVM's HotSpot; instead it is tuned specifically for running huge numbers of tiny, isolated processes.

Key jobs the BEAM handles for you:

  • Scheduling millions of lightweight processes across OS threads
  • Per-process garbage collection, so one process pausing to collect never stalls another
  • Preemptive scheduling based on reduction counts, so no single process can hog a core
  • Loading and swapping compiled modules while the system keeps running

Because each process has its own small heap and stack, BEAM garbage collection pauses are typically measured in microseconds and affect only that one process, not the whole node.

How does the BEAM garbage collector behave?
What primarily keeps a single Erlang process from monopolizing a CPU core on the BEAM?

3. What are processes in Erlang?

An Erlang process is the basic unit of concurrency — not an OS process, but a lightweight, BEAM-managed unit with its own stack, heap, and mailbox. Spawning one costs only a few hundred bytes and microseconds, so it's normal for a running system to have tens of thousands of them at once.

Processes share no memory. The only way for two of them to interact is by sending messages, which get copied into the receiver's mailbox. This isolation is what makes one process crashing harmless to everything else.

Pid = spawn(fun() -> io:format("hello from a process~n") end).

spawn/1 returns a PID immediately; the function body runs concurrently and independently of the caller.

What do two Erlang processes share directly in memory?
Roughly how expensive is spawning a new Erlang process compared to an OS thread?

4. What is the actor model in Erlang?

The actor model treats every concurrent unit as an actor: an isolated entity that has its own state, receives messages one at a time from a mailbox, and reacts by updating its own state, sending messages to other actors, or spawning new ones. Erlang's process model is a direct, production-grade implementation of this idea.

Three properties make it work in practice:

  1. Isolation — no shared mutable state between actors
  2. Asynchronous messaging — sends never block the sender
  3. Location transparency — sending to a PID looks the same whether the actor is local or on another node

Because there's no shared memory to lock, there are no mutexes or deadlocks between actors — the classic concurrency bugs of thread-and-lock programming simply don't apply at this level.

In the actor model, how does one actor typically communicate with another?
Why do actor-model systems avoid classic mutex/deadlock bugs?

5. What are atoms in Erlang?

An atom is a constant whose only value is its own name — think of it as a symbolic label rather than a variable. Atoms are written lowercase (ok, error, undefined) or quoted if they contain spaces or start with a capital ('My Atom').

They're used everywhere as tags: function results like {ok, Value} or {error, Reason}, process states, and message types all lean on atoms so the receiving code can pattern-match on a clear, self-documenting label instead of a raw number or string.

Atoms are stored once in a global atom table and compared by reference, so matching on them is essentially an integer comparison — fast, regardless of how long the atom's name is.

What does the value of an atom represent?
Why is comparing two atoms fast in Erlang?

6. What are tuples in Erlang?

A tuple is a fixed-size, ordered container written with curly braces, like {ok, 42} or {point, 3, 5}. Once created, its size and the position of each element never change — you build a new tuple rather than mutate an existing one.

Tuples are the standard way to bundle a small, known number of related values, especially as tagged return values:

{ok, Result} = file:read_file("data.txt").
{error, Reason} = file:read_file("missing.txt").

Because the shape is fixed, pattern matching against a tuple is a compile-time-checkable, constant-time operation — the runtime just checks the tag and unpacks the known positions, unlike walking a list.

What best describes an Erlang tuple?
Why is matching against a tuple typically faster than scanning a list?

7. What are lists in Erlang?

A list is a variable-length, ordered sequence written in square brackets, such as [1, 2, 3] or [apple, banana]. Internally it's a singly linked chain of cons cells — each cell holds a head element and a pointer to the rest of the list, ending in the empty list [].

That linked structure is why prepending is cheap and appending to the end is not:

[Head | Tail] = [1, 2, 3],   % Head = 1, Tail = [2, 3]
New = [0 | [1, 2, 3]].       % O(1) prepend -> [0,1,2,3]

Because lists have no fixed length, they're the natural fit for sequences of unknown or varying size, while tuples suit fixed-shape data. Most list processing in Erlang is written recursively, walking the head/tail structure one element at a time, or via list comprehensions.

How is an Erlang list represented internally?
Which operation is cheap on an Erlang list?

8. What is pattern matching in Erlang?

Pattern matching is Erlang's core mechanism for both destructuring data and controlling program flow. The = operator isn't assignment in the usual sense — it asserts that the left-hand pattern must match the right-hand value, binding any unbound variables in the process.

{ok, Value} = {ok, 42},      % Value becomes 42
{ok, Value2} = {error, oops} % fails: patterns don't match -> exception

The same mechanism drives function clause selection and case expressions: Erlang tries each clause's pattern in order and runs the first one that matches, extracting variables as it goes.

describe(0) -> "zero";
describe(N) when N > 0 -> "positive";
describe(_) -> "negative".

This removes the need for most manual type-checking or field-extraction code you'd otherwise write by hand.

What happens if the left and right sides of `=` don't structurally match in Erlang?
How does Erlang choose which function clause to run for a call?

9. Define immutability in Erlang?

In Erlang, once a variable is bound to a value it can never be reassigned within that scope — there is no mutation, only rebinding through a new variable name or a new function call. This applies to every data structure: lists, tuples, and maps are all persistent; "changing" one actually builds a new copy with the edit applied.

X = 5,
X = 6.   %% fails: X is already bound to 5, this is NOT reassignment

Immutability removes an entire category of bugs — no function can silently corrupt data another function is holding a reference to, and no two processes can race over the same mutable cell, since there isn't one.

The tradeoff is that "updating" a large structure means allocating a new version of it, so Erlang leans on structural sharing and per-process garbage collection to keep that cheap in practice.

What happens when you try to bind an already-bound Erlang variable to a different value?
What is the practical benefit of immutable data structures in Erlang?

10. What is a PID?

A PID (process identifier) is the opaque reference every Erlang process gets when it's spawned. You can't inspect or construct one by hand — you receive it from spawn/1 (or a related function) and use it as the destination for messages.

Pid = spawn(fun loop/0),
Pid ! {greet, "hi there"}.

PIDs work the same whether the target process lives on the local node or on a remote one in a distributed Erlang cluster — sending a message to a remote PID looks identical to sending to a local one, which is what gives Erlang its location-transparent messaging.

A PID remains valid only for the life of that process; once the process exits, the PID becomes a dead reference and sending to it is silently a no-op (unless the sender is linked or monitoring it).

How do you typically obtain a PID in Erlang?
What makes Erlang messaging 'location transparent'?

11. What is a module in Erlang?

A module is Erlang's unit of code organization — a single .erl file compiling to one unit that groups related functions under a shared name. The file starts with a -module(Name). declaration matching the filename, and an -export([...]). attribute listing which functions outside code may call.

-module(math_utils).
-export([square/1]).

square(N) -> N * N.

Functions not listed in -export are private to the module. Calling a function in another module uses the Module:Function(Args) syntax, e.g. math_utils:square(4).

Modules can also be hot-swapped: the BEAM can load a new version of a module into a running system while old processes still executing the previous version finish naturally, which underpins Erlang's live upgrade story.

What does the -export attribute control in an Erlang module?
How do you call a function defined in a different module?

12. What are guards used for in Erlang?

A guard is an extra boolean condition attached to a function clause or case branch with the when keyword. It runs after the pattern matches structurally, letting you filter on values rather than just shape.

classify(N) when N < 0 -> negative;
classify(0) -> zero;
classify(N) when N > 0 -> positive.

Guards are intentionally restricted to a small set of safe, side-effect-free built-ins (comparisons, arithmetic, type checks like is_integer/1, and a few guard-only BIFs) — you cannot call arbitrary functions in a guard. This restriction guarantees a guard can never crash or block, so the runtime can always safely evaluate it while deciding which clause to run.

What can a guard expression NOT do in Erlang?
When does a guard get evaluated relative to pattern matching?

13. What is OTP?

OTP (Open Telecom Platform) is the standard library and set of design principles bundled with Erlang for building reliable systems. It's not a separate product — it ships with the language and supplies battle-tested behaviors (reusable process templates) so teams stop reinventing server loops, supervision trees, and finite state machines by hand.

The core building blocks include:

  • gen_server — generic client/server process
  • supervisor — monitors and restarts child processes
  • gen_statem — generic finite state machine
  • application — packaging and lifecycle for a deployable unit

Using OTP behaviors means your business logic plugs into a well-tested process skeleton instead of hand-rolling message loops, which is why virtually all production Erlang systems are built on top of it.

What is OTP in the Erlang ecosystem?
Which OTP behavior is used to monitor and restart child processes?

14. What is a gen_server?

gen_server is the OTP behavior for the common client/server pattern: a process that holds state and responds to requests. Instead of writing your own receive loop, you implement a small set of callbacks and the behavior handles the process mechanics.

init(Args) -> {ok, State}.
handle_call(Request, From, State) -> {reply, Reply, NewState}.
handle_cast(Msg, State) -> {noreply, NewState}.

handle_call/3 is for synchronous requests where the caller waits for a reply; handle_cast/2 is for fire-and-forget messages. Callers interact through gen_server:call/2 and gen_server:cast/2 rather than sending raw messages, which keeps the protocol consistent and lets OTP add timeouts, tracing, and supervision for free.

Which gen_server callback handles a synchronous request that expects a reply?
What is the main benefit of writing a gen_server instead of a hand-rolled receive loop?

15. What is a supervisor?

A supervisor is an OTP process whose only job is to start, monitor, and restart a set of child processes according to a restart strategy. It doesn't do business logic itself — it exists purely to keep its children alive.

init([]) ->
    {ok, {{one_for_one, 5, 10},
          [{worker1, {my_worker, start_link, []},
            permanent, 5000, worker, [my_worker]}]}}.

Common strategies include one_for_one (restart only the child that died), one_for_all (restart every sibling if one dies), and rest_for_one (restart the failed child and any children started after it). Chaining supervisors into a tree, with workers as leaves, is what gives Erlang systems their self-healing behavior after a crash.

What is the sole responsibility of a supervisor process?
Which restart strategy restarts every sibling when one child crashes?

16. Describe the "let it crash" philosophy?

"Let it crash" means Erlang code generally doesn't try to defensively handle every possible failure inline. Instead, when a process hits an unexpected state, it's allowed to terminate, and a supervisor detects that exit and restarts it fresh.

The reasoning: trying to anticipate and recover from every edge case inline bloats code and often masks bugs by leaving the process in a half-corrupted state. Restarting from a clean init/1 is usually safer and simpler than trying to patch a process back to a known-good state after something goes wrong.

This doesn't mean errors are ignored — it means error handling is separated from business logic and pushed up to a supervisor, which is exactly what supervision trees are designed for. It's why gen_server callbacks are often written for the "happy path" and let genuinely exceptional input simply crash the process.

What is the main idea behind 'let it crash'?
Why is restarting a crashed process often preferred over patching its state in place?

17. What are records in Erlang?

A record is a compile-time convenience for naming the fields of a tuple, so you write Person#person.name instead of remembering that the name sits at tuple position 2. Under the hood a record is still a plain tuple with the record name as its first element.

-record(person, {name, age = 0}).

P = #person{name = "Ada", age = 34},
Age = P#person.age.

Records are defined with -record(Name, {Field1, Field2 = Default, ...}), usually in a header file so multiple modules can share the definition. Because they compile down to tuple access, using records costs nothing at runtime over a raw tuple — the benefit is purely readability and safer field access at compile time.

What is an Erlang record actually implemented as under the hood?
What is the main benefit of using a record instead of a raw tuple?

18. What is message passing?

Message passing is how Erlang processes exchange information, since they share no memory. The ! operator sends a message asynchronously into the target process's mailbox, and receive pulls a matching message out of the caller's own mailbox.

Pid ! {self(), {add, 2, 3}},
receive
    {ok, Result} -> Result
after 5000 ->
    timeout
end.

Sending never blocks the sender, even if the receiving process is busy — the message just waits in the mailbox. receive scans the mailbox for the first message matching one of its patterns, skipping over (but not discarding) messages that don't match, and can specify an after clause to avoid waiting forever.

What happens to the sender when it sends a message with `!` to a busy process?
What does the `after` clause in a `receive` block do?

19. What is hot code loading?

Hot code loading (or hot code swapping) is the BEAM's ability to load a new version of a module into a running system without stopping it. The runtime can keep two versions of a module in memory at once: the old version, still running for processes mid-execution, and the new version for anything freshly called.

Processes that were running old code keep running it until they make a fully-qualified call back into their own module (e.g. ?MODULE:loop(State)), at which point they switch to the new version. This "purge on next reference" model is why long-running server loops are typically written to explicitly call back into themselves.

This capability is what let early telecom switches be patched for bug fixes without dropping active calls, and it's still used today for rolling upgrades in systems that can't tolerate downtime.

How many versions of a module can the BEAM keep loaded at the same time?
What typically triggers a running process to switch to the new version of its own module?

20. What are binaries/bitstrings in Erlang?

A bitstring is a sequence of raw bits; a binary is the common special case whose length is a multiple of 8 bits (whole bytes). They're the go-to type for network data, files, and any bulk binary payload, written between double angle brackets.

Bin = <<1, 2, 3>>,
<<A:8, B:8, Rest/binary>> = Bin.   %% A = 1, B = 2, Rest = <<3>>

The bit-syntax pattern above is one of Erlang's standout features: you can match and slice binary protocol fields — fixed widths, variable-length trailing sections, even non-byte-aligned bit widths — directly in a pattern, without manual bit-shifting code.

Binaries over 64 bytes are also reference-counted and stored off the process heap, so passing a large binary between processes doesn't require copying its contents, only the reference.

What distinguishes a 'binary' from the more general 'bitstring' in Erlang?
Why can large binaries be passed between processes cheaply?

21. What is ETS?

ETS (Erlang Term Storage) is an in-memory table built into the BEAM for storing large amounts of data that any process can read or write directly, bypassing normal message passing. It's how Erlang handles shared state that would be awkward to keep inside a single process's mailbox loop.

Tab = ets:new(my_table, [set, public]),
ets:insert(Tab, {key1, "value1"}),
ets:lookup(Tab, key1).   %% -> [{key1, "value1"}]

Tables come in four flavors — set, ordered_set, bag, and duplicate_bag — controlling whether keys are unique and whether entries are ordered. Access is close to O(1) for set/bag tables since they're hash-based, which makes ETS a common choice for caches and lookup tables shared across many processes.

What is the main purpose of an ETS table?
Which ETS table type enforces unique keys with no ordering guarantee?

22. What is Mnesia?

Mnesia is Erlang's built-in distributed database, layered on top of ETS/DETS storage. Unlike ETS, Mnesia adds transactions, replication across nodes, and optional disk persistence, so multiple nodes in a cluster can share and safely update the same tables.

mnesia:create_table(person, [{attributes, record_info(fields, person)}]),
mnesia:transaction(fun() ->
    mnesia:write(#person{name = "Ada", age = 34})
end).

Because table rows are ordinary Erlang records/terms and queries are written in Erlang itself (or via mnesia:select/2 match specifications), there's no separate query language to learn the way SQL sits apart from application code.

It fits well for cluster-local configuration and session data where you want ACID-ish transactions without standing up an external database, though very large datasets or complex relational queries are usually better served by a dedicated external database.

What does Mnesia add on top of plain ETS tables?
How are Mnesia queries typically written?

23. What are list comprehensions?

A list comprehension builds a new list by describing it declaratively: a result expression, a generator that walks a source list, and optional filters, all in one line, instead of writing an explicit recursive function.

Squares = [X * X || X <- [1, 2, 3, 4], X rem 2 =:= 0].
%% Squares = [4, 16]

Reading that right to left: take each X from the list, keep only the ones where X rem 2 =:= 0 (the filter), then apply X * X to build the result. Multiple generators and filters can be chained, and comprehensions can also build binaries with << ... || syntax.

They're commonly reached for whenever you'd otherwise write a small "map + filter" recursive helper, since the intent reads clearly at a glance.

In `[X*X || X <- List, X > 0]`, what role does `X > 0` play?
What common hand-written construct does a list comprehension usually replace?

24. What is a behavior in Erlang?

A behavior is a formalized process pattern: OTP defines the generic, reusable parts (the message loop, error handling, standard API), and you supply a callback module that fills in the domain-specific parts. It's conceptually close to an interface plus a template method pattern from OOP.

-module(my_server).
-behaviour(gen_server).

init(Args) -> {ok, Args}.
handle_call(_Req, _From, State) -> {reply, ok, State}.

The -behaviour(gen_server) attribute tells the compiler which callbacks your module must export, and it will warn you at compile time if one is missing. The built-in behaviors are gen_server, gen_statem, gen_event, and supervisor, though you can also define custom behaviors for your own reusable patterns.

What does declaring `-behaviour(gen_server)` in a module do?
Which of these is NOT one of OTP's standard built-in behaviors?

25. What are ports in Erlang?

A port is Erlang's built-in mechanism for talking to an external OS process or native code, treated from the Erlang side just like another process you can send messages to and receive messages from. It's the standard, safe way to shell out to a C program, a script, or any external executable.

Port = open_port({spawn, "sort"}, [binary]),
Port ! {self(), {command, <<"banana\napple\n">>}}.

Communication happens over the external program's standard input/output, with Erlang handling the byte framing. Crucially, if the external program crashes, only the port dies — it cannot corrupt or crash the BEAM itself, which is what makes ports a safer integration point than loading foreign code directly in-process.

What does an Erlang port primarily let you do?
What happens to the BEAM if the external program behind a port crashes?

26. What is a NIF (Native Implemented Function)?

A NIF is a function implemented in C (or another native language) and loaded directly into the BEAM, callable from Erlang exactly like an ordinary function. It's used when pure Erlang is too slow for a specific hot path, such as heavy numeric or cryptographic work.

-module(math_nif).
-export([fast_add/2]).
-on_load(init/0).

init() -> erlang:load_nif("./math_nif", 0).
fast_add(_A, _B) -> erlang:nif_error(not_loaded).

The catch: a NIF runs inside the scheduler thread, not as a separate lightweight process, so a slow or crashing NIF can stall that scheduler or even bring down the entire node — it doesn't get the isolation a regular Erlang process gets. Because of that risk, NIFs are meant for short, fast operations, with long-running native work usually handed off to a port or a dirty scheduler instead.

Why must a NIF be kept short and fast?
How does a NIF differ from an Erlang process in terms of isolation?

27. How do you spawn a process in Erlang?

The base function is spawn/1 (or spawn/3 for a module/function/args form), which starts a new process running the given function and immediately returns its PID to the caller — there's no waiting for it to finish.

Pid1 = spawn(fun() -> loop(0) end),
Pid2 = spawn(counter, loop, [0]).

In practice, raw spawn/1 is rarely used directly in production code. Instead, teams reach for spawn_link/1 (to tie the new process's failure to the caller's), or more commonly gen_server:start_link/3, so the new process is registered with a supervisor and follows OTP's standard startup contract instead of being an unsupervised, untracked process.

What does `spawn/1` return to the calling process?
Why is raw spawn/1 rarely used directly in production OTP code?

28. How does Erlang achieve concurrency without shared memory?

Instead of multiple threads reading and writing the same memory region, each Erlang process owns its own private heap and stack, and the only channel between processes is copying messages into a mailbox. There's nothing to lock because there's nothing shared to race over.

Concretely, the BEAM's scheduler runs many lightweight processes across a small pool of OS threads (usually one per core), switching between them based on a reduction counter rather than OS-level time slicing. Since each process's data is private, the scheduler can safely preempt and resume any process without worrying about partial writes another process might see.

The cost of this design is that sharing large data between processes means copying it (unless it's a reference-counted off-heap binary), which is a deliberate trade: safety and fault isolation over the raw throughput of shared-memory threading.

What replaces locks/mutexes as the coordination mechanism between Erlang processes?
What is the main tradeoff of Erlang's shared-nothing concurrency model?

29. What is the difference between spawn and spawn_link?

Both start a new process and return its PID, but they differ in what happens when that new process terminates abnormally.

spawnspawn_link
Caller and new process are independent; if the child crashes, the caller is unaffected. Caller and new process are linked; if either crashes, the other receives an exit signal too.
You must set up monitoring manually to be notified of a crash. By default, an abnormal exit propagates and kills the linked process unless it's trapping exits.

Because a crash silently propagating can be exactly what you want (fail the whole group together) or exactly what you don't (an isolated worker), OTP supervisors use spawn_link internally combined with process_flag(trap_exit, true), so the supervisor is notified of the crash instead of being killed by it.

By default, what happens to the caller if a process started with spawn_link crashes?
How do supervisors typically avoid being killed by a linked child's crash?

30. What is the difference between a list and a tuple?

Both are ordered Erlang data structures, but they're built and used very differently.

ListTuple
Variable length; a linked chain of head/tail cons cells. Fixed length, decided at creation; contiguous positions.
Cheap to prepend to ([X|List]); processed recursively. Cheap to access any known position directly, e.g. element(2, T).
Good for sequences of unknown/variable size. Good for fixed-shape records like {ok, Value}.

A rule of thumb: if you don't know how many elements you'll have, or the count will vary, use a list. If the shape and number of fields is fixed and known up front, a tuple (often tagged with a leading atom) is the better fit.

Which structure would you choose for an unknown, possibly-growing sequence of items?
What is a common way to access one specific field out of a tuple?

31. Why is Erlang considered fault-tolerant?

Fault tolerance in Erlang comes from combining process isolation with supervision, not from trying to prevent every possible bug. Because each process has its own heap and crashes independently, a failure in one part of the system doesn't corrupt shared state or take down unrelated processes.

On top of that isolation, supervisors watch for process exits and restart them according to a defined strategy, so the system as a whole recovers automatically from failures that would otherwise require manual intervention. This is often summarized as designing for failure rather than around it — the system expects things to crash and has a built-in recovery path, instead of assuming failures can all be prevented.

This combination is why Erlang systems can report "nine nines" style uptime figures — not because the code never has bugs, but because individual bugs stay contained and self-heal quickly.

What two mechanisms combine to give Erlang its fault tolerance?
What does 'designing for failure' mean in the Erlang context?

32. How does pattern matching differ from equality comparison?

Equality comparison (== or =:=) asks a yes/no question: are these two already-known values the same? Pattern matching (=, function clauses, case) does more — it can also bind unbound variables as a side effect of a successful match, and it can check structural shape rather than a single scalar value.

5 =:= 5.              %% true, plain equality
{ok, X} = {ok, 42}.   %% match succeeds AND binds X = 42
{ok, X} = {error, _}. %% match fails (badmatch), not just "false"

Another subtlety: a failed equality check just evaluates to false, while a failed pattern match (outside a guarded context) raises a badmatch exception. That's why pattern matching is used for control flow — a mismatch is treated as an exceptional condition, not a normal boolean outcome.

What can pattern matching do that plain equality comparison cannot?
What happens when a pattern match like `{ok, X} = {error, foo}` fails?

33. When should you use ETS instead of process state?

Keeping data inside a single process's state works fine when only that process needs it, or when access is naturally serialized through that process's mailbox. ETS becomes the better fit once many processes need concurrent read/write access to the same data, because going through one process's mailbox would turn it into a bottleneck that every reader has to queue behind.

Typical ETS use cases: a shared cache read by hundreds of request-handling processes, a session table, or a routing table that changes occasionally but is read constantly. Process state is the better fit when the data is truly private to one actor's logic, or when you want writes serialized one-at-a-time as a deliberate correctness guarantee (e.g. a bank account balance).

The tradeoff: ETS reads/writes bypass message-passing isolation, so multiple processes can race on the same key unless you design your access pattern (or use ets:update_counter/3 style atomic ops) carefully.

When does ETS clearly beat keeping data in one process's state?
What isolation guarantee do you give up by using ETS instead of process state?

34. How do you handle errors in Erlang without try/catch?

Erlang's idiomatic style leans on tagged return values for expected, recoverable failure conditions, reserving exceptions (and try/catch) for genuinely unexpected situations. A function that can fail in an "ordinary" way typically returns {ok, Value} or {error, Reason}, and the caller pattern-matches on the outcome.

case file:read_file("config.txt") of
    {ok, Data} -> process(Data);
    {error, Reason} -> log_error(Reason)
end.

This keeps the "unhappy path" visible in the type of the return value rather than hidden in a control-flow jump, and it composes naturally with pattern matching in function clauses. Genuinely exceptional, programmer-error-style failures (a failed match, a bad arithmetic operation) are left to crash the process and get handled by a supervisor instead — which is the "let it crash" side of Erlang's error strategy.

What is the idiomatic way to signal an expected, recoverable failure in Erlang?
What category of failure is generally left to crash the process instead of being caught?

35. Why doesn't Erlang have mutable variables?

Erlang was designed around concurrent, isolated processes from day one, and mutable shared variables are exactly what makes concurrent programming dangerous in most other languages — race conditions, torn reads, and the need for locks all stem from two threads touching the same mutable cell.

By making every binding permanent once set, Erlang removes that entire hazard class at the language level: there's no mutable cell for two processes to race over, so there's nothing to protect with a mutex. It also makes reasoning about a function's behavior simpler, since a variable's value can't have silently changed between two lines that reference it.

The design does shift work elsewhere: "updating" a value means creating a new binding (often via recursion carrying an accumulator, or storing the "current" version in ETS/process state), rather than assigning in place. That's a deliberate trade of some ergonomic convenience for much simpler concurrent reasoning.

What concurrency hazard does immutability eliminate at the language level?
How do Erlang programs typically represent a value that 'changes over time'?

36. What is the difference between gen_server and gen_statem?

Both are OTP behaviors for a stateful process, but they model that state differently.

gen_servergen_statem
State is an arbitrary term you pass between callbacks; behavior is the same regardless of what that term holds. State is explicitly modeled as one of a set of named states, with transitions defined between them.
Good fit when logic is mostly "handle this request, update some data." Good fit when the process genuinely moves through distinct modes, e.g. connecting → authenticating → ready.

You can hand-roll a state machine inside a gen_server using a state-tagged tuple and a big case, but as the number of states and valid transitions grows, gen_statem's explicit state/event/action model tends to keep the logic easier to follow and harder to get into an invalid transition.

What is the key structural difference between gen_server and gen_statem state handling?
When is gen_statem typically the better fit over gen_server?

37. How do you implement a simple supervision tree?

A supervision tree starts with a top-level supervisor whose init/1 callback declares a restart strategy and a child specification list; each child can itself be a worker or another supervisor, nesting arbitrarily deep.

init([]) ->
    SupFlags = #{strategy => one_for_one, intensity => 5, period => 10},
    Children = [
        #{id => cache_worker,
          start => {cache_worker, start_link, []},
          restart => permanent,
          type => worker}
    ],
    {ok, {SupFlags, Children}}.

Starting the top supervisor with supervisor:start_link/3 automatically starts every declared child in order. If cache_worker crashes, the one_for_one strategy restarts just that child (up to 5 times per 10 seconds, per intensity/period); exceeding that threshold escalates the crash to the supervisor's own parent.

What do the `intensity` and `period` values in SupFlags control?
What happens if a supervisor exceeds its configured restart intensity?

38. What happens when a linked process crashes?

When a linked process terminates abnormally, the BEAM sends an exit signal carrying the exit reason to every process it's linked to. What happens next depends on whether the receiving process is trapping exits.

process_flag(trap_exit, true),
Pid = spawn_link(fun() -> exit(boom) end),
receive
    {'EXIT', Pid, Reason} -> io:format("child died: ~p~n", [Reason])
end.

By default, a process receiving a non-normal exit signal also terminates with that same reason, propagating the failure outward — which is exactly how a crash in one worker can cascade up to a supervisor. If the receiving process has called process_flag(trap_exit, true), the exit signal instead arrives as an ordinary {'EXIT', Pid, Reason} message it can pattern-match on and react to, which is precisely how supervisors detect child crashes without being killed by them.

By default, what happens to a process when a linked process it's connected to crashes?
How does trap_exit change this behavior?

39. When would you choose Mnesia over ETS?

Reach for Mnesia when data needs to survive beyond a single node, or beyond a single BEAM restart, or must be updated as part of a transaction spanning multiple tables. Reach for plain ETS when the data is node-local, disposable if the node restarts, and speed matters more than durability or cross-node consistency.

ETSMnesia
In-memory only, single node.Optional disk persistence, replicated across cluster nodes.
No built-in transactions across keys/tables.ACID-style transactions across multiple tables.
Slightly lower overhead per operation.Extra overhead for transaction coordination and replication.

A common pattern is actually using both: ETS as a fast local cache in front of Mnesia's authoritative, replicated copy, refreshing the cache when the underlying data changes.

Which scenario clearly favors Mnesia over ETS?
What common pattern combines ETS and Mnesia together?

40. How do you use the observer tool to inspect a running system?

observer is OTP's built-in GUI for peering into a live BEAM node: process list with memory and message queue length per process, supervision tree visualization, ETS table browser, and application overview, all without adding any extra code to the system being inspected.

1> observer:start().

Running that in a shell attached to (or connected to) the node you want to inspect opens the GUI. The Applications tab is often the fastest way to spot trouble: it renders the live supervision tree, so a worker stuck restarting in a crash loop, or a process with a mailbox that keeps growing, is visible at a glance rather than something you have to dig for with ad hoc shell commands.

For headless/remote servers without a GUI, the same information is available via shell functions like i/0, process_info/2, and ets:i/0, or via the newer observer_cli terminal-based alternative.

What can the observer tool's Applications tab show you at a glance?
How would you inspect a running BEAM node without a GUI available?

41. Explain the execution flow of a gen_server call?

gen_server:call/2 looks like a normal function call, but underneath it's a request/reply protocol built out of plain message passing plus a monitor for safety.

sequenceDiagram
    participant Caller
    participant GenServer as gen_server process
    Caller->>GenServer: {'$gen_call', {Caller,Ref}, Request}
    Note over GenServer: handle_call/3 runs
    GenServer-->>Caller: {Ref, Reply}
    Note over Caller: call/2 unwraps Reply and returns it

Step by step: the caller sends a tagged message containing its own PID and a fresh unique reference, then sets up a monitor on the server and blocks in a receive waiting for a reply tagged with that same reference (with a default 5-second timeout). The server's handle_call/3 callback runs, returning {reply, Reply, NewState}, and the framework sends Reply back tagged with the original reference. The monitor exists so that if the server dies mid-call, the caller gets a 'DOWN' message instead of hanging forever.

What does gen_server:call/2 use to avoid hanging forever if the server dies mid-call?
What ties a specific reply message back to the correct waiting caller?

42. Explain the internal working of the BEAM scheduler?

The BEAM typically runs one scheduler thread per CPU core, and each scheduler maintains its own run queue of processes ready to execute. Rather than relying on OS-level time slicing, the BEAM preempts processes based on a reduction count — roughly one reduction per function call/operation — giving each process a budget (around 2000 reductions) before it's paused and put back at the end of the run queue.

flowchart LR
    A[Run queue: Process A, B, C] --> B[Scheduler picks Process A]
    B --> C{Reduction budget exhausted or process waits?}
    C -->|Yes| D[Process A suspended, requeued]
    C -->|No, finished naturally| E[Process A removed from queue]
    D --> A

This makes scheduling fair and predictable regardless of what an individual process does — a process stuck in a tight loop can't starve others, since it gets preempted at the reduction boundary just like everyone else. Schedulers also periodically rebalance work across cores via work stealing, so a scheduler that runs out of ready processes can pull some from a busier neighbor's queue instead of sitting idle.

What mechanism does the BEAM use to preempt a running process, instead of OS time slicing?
How does the BEAM keep schedulers balanced across CPU cores?

43. How can you optimize message passing between heavy processes?

The main cost in Erlang message passing is copying the message into the receiver's mailbox, so optimization mostly means reducing what gets copied and how often.

  • Use off-heap binaries for large payloads — binaries over 64 bytes are reference-counted, so passing one only copies a small reference, not the data itself.
  • Batch messages instead of sending many tiny ones; fewer, larger messages amortize per-message scheduling and copy overhead.
  • Avoid large process state that gets copied unnecessarily during garbage collection triggered by message arrival.
  • Watch mailbox growth — a slow consumer with a growing mailbox means the sender is outpacing the receiver; back-pressure (e.g. via gen_server:call instead of cast) can force the sender to wait rather than flood the queue.

For very hot paths, keeping data in ETS and passing only a key/reference between processes avoids the message-copy cost entirely, at the price of losing per-message isolation for that data.

Why does passing a large binary between processes stay cheap?
How can you apply back-pressure to a producer that's overwhelming a slow consumer?

44. Explain the lifecycle of a supervised process?

A supervised worker follows a well-defined path from start to eventual restart or shutdown, orchestrated entirely by its supervisor.

stateDiagram-v2
    [*] --> Starting: supervisor calls start_link
    Starting --> Running: init/1 returns {ok, State}
    Running --> Crashed: unhandled error / exit
    Running --> Terminating: normal shutdown requested
    Crashed --> Starting: supervisor restarts per strategy
    Crashed --> GivingUp: restart intensity exceeded
    Terminating --> [*]
    GivingUp --> [*]: supervisor itself terminates/escalates

On start_link, the supervisor blocks until init/1 replies (so a failed startup is caught immediately, not silently). Once running, the process handles calls/casts/info messages until it either shuts down normally, or crashes. A crash triggers the configured restart strategy (one_for_one/one_for_all/rest_for_one); if crashes happen faster than the configured intensity/period allows, the supervisor gives up and escalates the failure to its own parent instead of looping forever.

Why does a supervisor block during start_link until init/1 replies?
What happens if a supervised process crashes faster than the configured restart intensity allows?

45. How do you troubleshoot process mailbox overflow?

A growing mailbox means a process is receiving messages faster than it processes them, which if left unchecked can exhaust memory and eventually crash the node. The first step is confirming which process(es) are affected.

Pid = whereis(my_worker),
{message_queue_len, Len} = process_info(Pid, message_queue_len).
%% or, node-wide, via recon: recon:proc_count(message_queue_len, 5).

Once you've identified the culprit, common causes and fixes include:

  1. Slow receive loop — the process is doing too much per message; profile and optimize the hot path, or offload work to helper processes.
  2. Unbounded producer — switch producers from cast to call so they block and naturally throttle instead of flooding the mailbox.
  3. Selective receive scanning cost — a receive with a narrow pattern has to skip past unmatched messages already in the queue; a mismatched, ever-growing backlog of unrelated messages makes each receive progressively slower.

Tools like observer's process view, or the recon library's proc_count/2, make it straightforward to spot the offending process across a whole running node rather than checking processes one at a time.

What is the direct cause of a growing message queue on a process?
How can switching a producer from cast to call help with mailbox overflow?

46. What is the difference between synchronous and asynchronous message passing in OTP?

Both ultimately use the same underlying ! primitive, but OTP's gen_server:call and gen_server:cast wrap it with very different guarantees about waiting for a result.

call (synchronous)cast (asynchronous)
Caller blocks until a reply arrives or it times out. Caller sends and continues immediately; no reply is expected.
Caller gets a monitor, so a server crash surfaces as an error rather than an infinite wait. No monitor by default; if the message is lost or the server crashes, the caller has no way to know.
Naturally provides back-pressure — a slow server slows down its callers. No back-pressure; a fast producer can flood a slow server's mailbox.

The rule of thumb: use call when the caller needs a result or needs to know the operation actually happened; use cast for fire-and-forget notifications where losing one occasionally, or not knowing immediately if the target is alive, is acceptable.

What safety net does gen_server:call have that gen_server:cast does not?
Which is the better fit for a fire-and-forget log notification where occasional loss is acceptable?

47. Which is better for state management, ETS or process dictionaries, and why?

A process dictionary (put/2, get/1) is per-process mutable key-value storage that bypasses Erlang's usual immutability, private to that one process only. ETS is a separate in-memory table that can be shared and accessed concurrently across many processes.

ETS is generally the better choice whenever more than one process needs the data, since the process dictionary is invisible outside its owning process and can't be shared without message passing anyway. Even within a single process, most experienced Erlang developers avoid the process dictionary except for narrow cases (some tracing/debugging helpers, or caching a value purely local to that process's own logic), because it silently breaks the functional, traceable style the rest of the language encourages — a function reading get(some_key) has a hidden dependency that isn't visible in its arguments.

So: ETS for anything shared or sizeable; process dictionary only for small, genuinely process-local state where passing it explicitly through function arguments would be needlessly awkward.

Who can access a process dictionary entry set with put/2?
Why do experienced Erlang developers generally avoid the process dictionary?

48. How does distributed Erlang handle node failures?

Distributed Erlang connects multiple BEAM nodes into a cluster where PIDs, links, and monitors all work transparently across node boundaries — sending to a remote PID looks identical to sending locally. Node health is tracked through a mesh of TCP connections, each node maintaining a heartbeat with the others.

sequenceDiagram
    participant A as Node A
    participant B as Node B
    A->>B: monitor_node(B, true)
    Note over A,B: Heartbeat over TCP
    B--xA: Connection lost / node down
    A->>A: receives {nodedown, 'B@host'}

When a remote node becomes unreachable (crash, network partition, or explicit shutdown), any local process that called monitor_node/2 on it receives a {nodedown, Node} message, and any processes linked to PIDs on that node get exit signals just as if those remote processes had crashed locally — the same link/monitor semantics that work within one node extend across the cluster.

Because Erlang can't distinguish "the remote node crashed" from "the network partitioned," distributed systems built on it still need an explicit strategy (e.g. quorum-based tools, or accepting eventual reconciliation) for split-brain scenarios — the platform gives you the failure notification, not a built-in resolution to network partitions.

What message does a process receive when a node it's monitoring becomes unreachable?
What can distributed Erlang NOT automatically distinguish between?
«
»
#

Comments & Discussions