What Hot Code Reloading Is
The BEAM VM can hold at most two versions of a compiled module in memory at once: the current version and one old version. When code:load_file/1 (or l/1 in the shell) loads a new version, it doesn't stop any running process — processes already executing inside that module keep running whichever version they were on until they make a call that resolves to the newest code. This is what makes hot code reloading possible: you can push a new module into a live system without restarting the node or dropping any in-flight connections.
Cricket analogy: The BEAM keeping an 'old' and 'new' version of a module resident is like a franchise keeping both last season's and this season's playing squad on the roster during a transfer window, only retiring the old squad once every match currently in progress has finished.
Local vs Fully Qualified Calls
The mechanism behind this is the distinction between local and fully-qualified calls. A local call like f(X) inside a loop resolves at the version the running process's code was compiled against and keeps executing that version even after a newer one loads. A fully-qualified call like Module:f(X) is resolved fresh on every invocation against whatever version is currently loaded. Long-running server loops are therefore often written to make an explicit fully-qualified 'tail call' back into themselves so they naturally pick up new code on their next iteration.
Cricket analogy: A local call staying on the old module version is like a bowler sticking to the exact run-up rhythm they started their over with, while a fully-qualified call is like checking the scoreboard between overs and immediately adopting the newly announced field restrictions.
%% counter.erl — a gen_server that supports hot code upgrade
-module(counter).
-behaviour(gen_server).
-export([start_link/0, bump/0]).
-export([init/1, handle_call/3, handle_cast/2, code_change/3]).
start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, 0, []).
bump() -> gen_server:call(?MODULE, bump).
init(N) -> {ok, N}.
handle_call(bump, _From, N) ->
{reply, N + 1, N + 1}.
handle_cast(_Msg, N) -> {noreply, N}.
%% Invoked automatically during a release upgrade so state
%% from the OLD version can be migrated to the NEW version's shape
code_change(_OldVsn, N, _Extra) when is_integer(N) ->
{ok, #{count => N, upgraded => true}}.Using appup and relup for Release Upgrades
For a coordinated production upgrade, each application version pair needs an .appup file describing exactly how to transition — instructions like {update, Module, {advanced, Extra}} for stateful modules or {load_module, Module} for stateless ones. rebar3's relx/relup tooling reads these .appup files and compiles them, together with the old and new releases, into a single .relup file. OTP's release_handler then applies that .relup on the running node, loading new code, migrating process state, and unloading the old version once the transition completes.
Cricket analogy: An .appup file listing precise upgrade instructions per module is like a team's tour dossier specifying exactly which players get added, dropped, or retained between two specific series, rather than a vague 'refresh the squad.'
The BEAM only ever keeps two versions of a module loaded — current and old. Loading a third version forces an implicit purge of the oldest, which kills any process still running purely on that version. In a hot upgrade, always drive long-lived processes like gen_servers through their code_change/3 callback so they migrate off the old version before the next reload.
Practical Considerations
Hot upgrades work cleanly when a module's exported behaviour and state shape are unchanged or the code_change/3 callback fully accounts for the difference; they get complicated quickly when a gen_server's internal state record changes shape, when ETS table structures change, or when several interdependent applications must upgrade in lockstep. Because of that operational complexity, many teams today prefer rolling deploys behind a load balancer for ordinary services, reserving true hot code upgrades for systems — like telecom switches — where dropping any in-flight process is unacceptable.
Cricket analogy: Relying on code_change/3 to migrate a gen_server's internal state is like a captain needing to physically hand over the exact match situation — score, overs left, field placements — to an incoming stand-in captain mid-match, not just swap the name on the team sheet.
code:soft_purge/1 removes an old module version only if no process is currently executing it, returning false (and leaving it in place) if a process is still running old code — safer than code:purge/1, which unconditionally kills any process still executing the purged version.
- The BEAM VM can hold at most two versions of a module in memory simultaneously: the current version and one old version.
- Local function calls within a running process continue on whichever module version was loaded when the process's code started executing.
- A fully-qualified call (Module:Function(Args)) always jumps to the newest loaded version of that module.
- code:load_file/1 (or l/1 in the shell) loads a new module version; code:purge/1 forcibly removes the old version, killing lingering processes.
- code:soft_purge/1 removes the old version only if no process is still executing it, avoiding an unsafe kill.
- .appup files describe per-version upgrade/downgrade instructions; rebar3's relup/relx tooling assembles them into a release upgrade package.
- gen_server implementations should handle code_change/3 to migrate internal state safely across a hot upgrade.
Practice what you learned
1. How many versions of a given module can the BEAM VM keep loaded in memory at once?
2. What determines whether a running process picks up a newly loaded module version?
3. What happens if a third version of a module is loaded while a process is still running purely on the oldest version?
4. Which gen_server callback is the standard hook for migrating a process's internal state during a hot code upgrade?
5. What is the role of an .appup file in a release upgrade?
Was this page helpful?
You May Also Like
Erlang and Rebar3
Learn how Rebar3 standardizes building, testing, and dependency management for Erlang projects.
Erlang and Distributed Systems
Explore how Erlang nodes connect, communicate transparently, and stay resilient across a distributed cluster.
Erlang and Mnesia
Understand Mnesia, Erlang's built-in distributed database, its table types, transactions, and how it fits into OTP applications.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics