Erlang / Erlang Advanced Interview questions
1. What is the difference between linking and monitoring a process?
Both let one process find out when another terminates, but they differ in direction and severity of the notification. A link is bidirectional and, by default, fatal: if either linked process crashes, the other receives an exit signal and dies too, unless it's trapping exits. A monitor ( erlang:mo...
2. Why would you choose monitor over link for a client process?
A client that calls into a server it doesn't own — say, a request handler calling a shared cache process — usually shouldn't die just because that server crashed. Using monitor/2 instead of link/1 gets you the crash notification without coupling the client's own lifecycle to the serve...
3. What is a dirty scheduler and when should a NIF use one?
A regular NIF runs inline on a normal BEAM scheduler thread, which is fine for short operations but dangerous for anything that blocks or takes more than roughly a millisecond, since it stalls that entire scheduler and every process waiting behind it. Dirty schedulers are a separate pool of OS th...
4. Explain the internal working of Erlang's per-process garbage collector?
Each Erlang process has its own private heap, so garbage collection happens independently, one process at a time, rather than as one global stop-the-world pause across the whole node. When a process's heap fills up (typically triggered by a message arriving or an allocation), the BEAM runs a gene...
5. Why does Erlang use generational (per-process) garbage collection instead of a single global GC?
A single global collector would need to pause every process on the node at once to safely scan and compact memory, which directly conflicts with Erlang's goal of soft real-time responsiveness — a telecom switch or messaging backend can't tolerate a multi-hundred-millisecond global pause jus...
6. What is EPMD and what role does it play in distributed Erlang?
EPMD (Erlang Port Mapper Daemon) is a small, lightweight process that runs once per host and acts as a name-to-port lookup service for Erlang nodes on that machine. Each node, on startup, registers its name and the TCP port it's listening on with the local EPMD; other nodes trying to connect firs...
7. Why do distributed Erlang nodes require a shared cookie?
The cookie is a shared secret string that every node in a cluster must present to authenticate itself before another node will accept a connection from it — without a matching cookie, a node can't join the cluster, send messages to remote PIDs, or make remote calls. %% set via command line ...
8. Explain the internal working of the Erlang distribution handshake between two nodes?
When one node connects to another, they don't just open a socket and start exchanging Erlang messages — they first perform a handshake to agree on protocol version and prove they share the same cookie, before the connection is trusted for actual traffic. sequenceDiagram participant A as Nod...
9. What is the two-version code loading rule and what happens when a third version is loaded?
The BEAM keeps at most two versions of a module in memory at once: the current version, which new calls resolve to, and the old version, still executing for any process that was mid-loop when the reload happened. This bounded window is what allows hot code upgrades without an unbounded pile-up of...
10. Explain the internal working of code:purge/1 and code:soft_purge/1?
Both functions remove the old (previous) version of a module from memory, freeing it up so a future reload has room, but they differ in how they treat processes still executing that old code. code:purge/1 code:soft_purge/1 Unconditionally kills any process still running the old version. Checks fi...
11. What is a gen_event and when would you choose it over gen_server?
gen_event is the OTP behavior for a pub/sub-style event manager: one process holds a list of independent handler callback modules, and any event sent to the manager is dispatched in turn to every registered handler, each with its own private state. { ok , Pid } = ge n _eve nt : s tart _li n k() ,...
12. What is the difference between error, exit, and throw in Erlang?
All three raise an exception that unwinds the call stack until caught, but they carry different intent and default severity. error/1 exit/1 throw/1 Signals a programming/runtime error; includes a stack trace; typically left uncaught to crash the process. Signals intentional process termination; p...
13. Why is exit/1 different from exit/2?
exit(Reason) (arity 1) terminates the calling process itself with the given reason, exactly like any other uncaught exception, and propagates that reason to any linked processes. exit(Pid, Reason) (arity 2) instead sends an exit signal to another process, asking it to terminate, without affecting...
14. What is an OTP application (.app file) and how does it differ from a single module?
An OTP application is a packaged, independently startable unit of functionality — typically a whole subsystem (a web server, a connection pool, a whole product component) — described by a .app file that declares its name, version, modules, registered processes, and dependencies on oth...
15. Explain the execution flow of application:start/1 and its dependency resolution?
Calling application:start(my_app) doesn't just run one function — it triggers a dependency-aware startup sequence managed by the application controller. flowchart TD A[application:start my_app] --> B{Dependencies in .app already running?} B -->|No| C[Start each missing dependency first, rec...
16. What is a release in OTP and how does it differ from an application?
A release bundles a specific set of applications (your own plus the OTP applications they depend on, like kernel , stdlib , and sasl ) together with a matching Erlang/OTP runtime version into one deployable, versioned unit — typically built with rebar3 release or similar tooling, producing ...
17. Explain the internal working of a relup-based hot upgrade?
A relup (release upgrade) file is a generated script describing exactly how to move a running system from one release version to another without a restart: which applications to stop/start, which modules to load, in what order, and which processes need their internal state transformed via code_ch...
18. What is the code_change/3 callback used for in gen_server?
code_change(OldVsn, State, Extra) is the hook OTP calls during a release upgrade or downgrade so a gen_server can transform its existing in-memory state to match whatever shape the new code version expects, instead of losing state by simply restarting. code_change(OldVsn, State, _Extra) when OldV...
19. How does the global module handle process name registration across a cluster?
The global module extends Erlang's local process registry ( register/2 ) across an entire distributed cluster: global:register_name/2 makes a PID discoverable by name from any connected node, not just the one it was spawned on. global :register_name(payment_processor, self()), %% from any node in...
20. Why can global name registration cause a network partition ("split brain") problem?
If a cluster's network splits into two isolated groups that can each still see their own members but not the other side, each half only knows about its own view of global names. A process on each side of the partition could register the same global name independently, since neither side can detec...
21. What is the pg (process groups) module used for?
pg lets multiple processes join a named group, cluster-wide, so a caller can broadcast or fan out work to every member without tracking individual PIDs itself — unlike global , which maps one name to exactly one process, pg maps one name to a set of processes. pg:join(chat_room_42, self()),...
22. What are Erlang maps and how do they differ from records?
A map is a dynamic key-value data structure, written #{Key => Value} , where keys can be any term and the set of keys is decided at runtime rather than fixed at compile time. A record is syntactic sugar over a tuple with a fixed, compile-time-known set of named fields. M = # {name => "Ada" , age ...
23. When should you choose a map over a record for structured data?
Reach for a record when the shape of the data is known and fixed at compile time and you want the compiler to catch typos in field names — classic examples are a gen_server's internal state or an internal domain struct that never gets serialized to an external format. Reach for a map when t...
24. What is the difference between ets:match, ets:select, and ets:foldl?
All three read multiple rows out of an ETS table, but they differ in expressiveness and performance characteristics. ets:match ets:select ets:foldl Simple pattern with '_' wildcards; returns matching bound variables. Full match specifications: pattern + guard conditions + result shape, compiled f...
25. How can you use match specifications to filter ETS data efficiently?
A match specification is a low-level, compiled query format — a list of {Pattern, Guards, Result} tuples — that ets:select/2 executes directly inside the table engine, so filtering happens without copying every row out to the calling process first. %% Find {Key, Value} pairs where Val...
26. What do the write_concurrency and read_concurrency ETS options optimize for?
By default, an ETS table uses a locking scheme tuned for a single, simple access pattern. The write_concurrency and read_concurrency options let you tell the table engine what kind of concurrent access to expect, so it can pick internal locking granularity accordingly. ets:new(my_table, [set, pub...
27. How can you optimize binary pattern matching for parsing variable-length network protocols?
Erlang's bit-syntax lets you decode a protocol header directly in a pattern, including fields whose length depends on an earlier field — a common shape in real network protocols (a length-prefixed payload, for instance). parse(<
28. Why is copying a large list between processes more expensive than copying a large binary?
A large binary (over 64 bytes) is stored off the process heap and reference-counted, so sending it in a message copies only a small pointer-sized reference — the actual byte data never moves. A list, by contrast, is a chain of individually heap-allocated cons cells with no such off-heap, re...
29. What is tail call optimization in Erlang and why does it matter for long-running loops?
A function call is a tail call when it's the very last operation in a clause — nothing happens with its result except returning it directly. Erlang recognizes this pattern and reuses the current stack frame instead of pushing a new one, so a tail-recursive function can loop indefinitely wit...
30. How do you write a properly tail-recursive accumulator-based function?
The standard technique is to carry the running result forward as an extra argument (an accumulator), so each recursive call is the last thing that happens — there's no pending arithmetic or list-building left to do after the call returns. %% Not tail-recursive: work (H + Rest) happens after...
31. Explain the internal working of selective receive and why message order in the queue matters?
A receive block doesn't necessarily take the first message in the mailbox — it scans the queue in arrival order looking for the first message that matches any of its clauses, skipping over (but leaving in place) messages that don't match, then removes only the one it matched. flowchart LR A...
32. What is erlang:process_flag(priority,...) used for?
Every Erlang process has a scheduling priority — low , normal (the default), high , or max — that influences how eagerly the scheduler picks it from the run queue relative to other ready processes on the same scheduler. process_flag(priority, high). Higher-priority processes get sched...
33. Why should high-priority processes be used sparingly in Erlang?
Erlang's whole scheduling model assumes most processes are roughly equal citizens sharing time fairly via reduction counting. Marking a process high or max priority breaks that assumption locally: it can consistently jump ahead of normal-priority work on the same scheduler, and if it runs frequen...
34. What is rpc:call/4 and how does it work across distributed nodes?
rpc:call(Node, Module, Function, Args) lets one node synchronously invoke a function on another connected node and get the result back, wrapping the underlying message-passing machinery so it looks like an ordinary function call. Result = rpc:call('nodeb@host', lists, sort, [[3,1,2]]). %% Result ...
35. What is the difference between rpc:call and simply sending a message to a remote PID?
Sending Pid ! Msg to a remote PID is a raw, asynchronous, fire-and-forget send — you get no return value unless the receiving process is written to reply, and you must already know that specific PID. rpc:call/4 is a higher-level convenience built on top of message passing that packages up "...
36. How does erlang:send/3 with the nosuspend option change message-sending behavior?
A plain Pid ! Msg send can, in rare cases, briefly suspend the sending process if the underlying distribution buffer to a remote node is full — effectively creating unwanted back-pressure on the sender. erlang:send(Pid, Msg, [nosuspend]) asks for the same send but refuses to suspend the cal...
37. What is Dialyzer and how does success typing differ from static typing?
Dialyzer is Erlang's static analysis tool for finding type discrepancies — but it works differently from a conventional static type checker like those in typed languages. Instead of requiring every value to be annotated and rejecting code that can't be proven correct up front, Dialyzer uses...
38. Why doesn't Dialyzer catch every type error the way a traditional type checker would?
Success typing is intentionally conservative: it only reports a call site when it can prove the function can never succeed with those argument types, based on inferring what the function's body could actually produce or accept. If a function's implementation is loose enough that a call is merely ...
39. How do you define and use a custom behavior with the -callback attribute?
Beyond OTP's built-in behaviors (gen_server, supervisor, and so on), you can define your own reusable process pattern by declaring the required callbacks a module implementing your behavior must export, using the -callback attribute. %% my_plugin.erl - the behavior definition -module(my_plugin). ...
40. What is the difference between a behavior callback module and a plain library module?
A plain library module (like lists or string ) exposes stateless functions you call directly whenever you want — there's no framework driving it, no lifecycle, no expected shape beyond whatever arguments and return value each function defines on its own. A behavior callback module instead p...
41. Explain the internal working of exception propagation through nested try/catch blocks?
An exception raised anywhere inside a try block unwinds the call stack looking for the nearest enclosing catch whose pattern matches the exception's class and reason — skipping over any intervening function calls that don't themselves catch it, exactly like exception handling in most langua...
42. When should you use throw instead of returning an {error, Reason} tuple?
Both signal "this didn't work," but they fit different shapes of control flow. A tagged {error, Reason} return keeps the failure visible in the function's ordinary return value, forcing every caller to explicitly pattern-match and decide what to do — it composes naturally with Erlang's usua...
43. How do you troubleshoot a memory leak caused by large binaries not being garbage collected?
Large (>64 byte) binaries live off the process heap and are reference-counted, but the counter only drops when the owning process's heap is actually garbage collected — a process that receives many large binaries but rarely triggers a GC cycle (because its own small heap of local variables ...
44. What is the significance of the binary reference count and off-heap binary garbage collection?
Any binary larger than 64 bytes is allocated once, off the process heap, and shared by reference rather than copied whenever it's passed around within reach of the same underlying data (for instance, sub-binary matches, or being stored in multiple variables). A reference count tracks how many liv...
45. Why is the Erlang distribution protocol not encrypted by default, and how can you secure inter-node traffic?
The standard distribution protocol (used for node-to-node traffic once the cookie handshake succeeds) sends Erlang term data in plaintext over TCP — it was designed assuming nodes sit on a trusted, private network, not a hostile or public one, so encryption wasn't built in as the default to...
46. Why is Mnesia's two-phase commit necessary for distributed transactions?
When a Mnesia transaction writes to tables replicated across multiple nodes, every replica must end up agreeing on the outcome — either all of them commit the change, or none of them do. Without coordination, a network hiccup could leave one node having applied the write while another hasn'...
47. How do you troubleshoot slow ETS lookups on a heavily-used table?
A "fast" O(1) ETS set / bag lookup can still degrade under real load for a handful of specific, diagnosable reasons rather than randomly — the first step is confirming which of these actually applies before reaching for a fix. ets:info(Tab, memory), %% table size, in words ets:info(Tab, siz...
48. Why might increasing the number of BEAM schedulers not improve throughput linearly?
Adding scheduler threads only helps if there's enough independent, ready-to-run work to actually fill them, and if nothing else becomes the new bottleneck once CPU contention eases. Several factors commonly cap the gains well before scheduler count matches core count: Contended shared resources &...