Performing a Zero‑Downtime Hot Code Upgrade in Erlang/OTP with Release Handler
Learn how to update an Erlang/OTP application on a live node using release_handler, appup files, and OTP’s built‑in rollback. Follow a step‑by‑step guide, verify the upgrade, and recover if something goes wrong.
27 Jul 2026, 02:48 UTC

Desired Outcome
Replace the running code and configuration of an OTP application while a node is live, without terminating processes or dropping in‑flight messages. After the upgrade, all processes should continue with the new module versions and the system should report the new release number.
Prerequisites
- OTP 25+ (or the specific OTP release you target).
- An application built as a release using
relxor OTP’sreleasestool. The release directory must containreleases/RELEASEwith aRELEASESfile. - Versioned modules: the old and new module files must be compiled with distinct
vsnattributes in the .app file. - An
appupfile for each application that describes the code change steps. A typicalappupcontains a list of tuples:{old_vsn, new_vsn, […] }. - A supervision tree that implements
code_change/3for each process that may be upgraded. If a process doesn’t supportcode_change, OTP will terminate it during the upgrade. - Manual migration logic for ETS tables or
persistent_termif your application stores state there, as OTP does not move these automatically. - Root or user permissions to read the release directories and write to the running node’s code path.
Focused Procedure
- Prepare the new release. Build the new release in a separate directory, e.g.
/opt/app/releases/2.0.0. Verify that theRELEASESfile lists the new version and that theappupfile is present.# Build with relx $ relx -c relx.config - Start the old release. Launch the node from the current release directory.
$ cd /opt/app/releases/1.5.0 $ ./bin/app start - Verify the current state. On the running node, check the loaded module and application version.
Expected output: module1> code:which_is_loaded(my_app_mod). 2> application:which_applications().my_app_modwith vsn{1,5,0}, applicationmy_appat version{1,5,0}. - Invoke the release handler. From the node shell or a separate terminal, run:
Parameters:1> release_handler:upgrade(\"/opt/app/releases/2.0.0\", [], [])ReleaseDir– path to the new release.Options– empty list for default behaviour.AppUpList– empty list lets OTP readappupfiles automatically.
- Load the new code into the code server.
- Apply each application’s
appupinstructions. - Call
code_change/3on all supervisors and workers. - Update
persistent_termentries if specified in theappup.
- Confirm the upgrade. After the call returns with
ok, run the same checks as before:
You should see vsn1> code:which_is_loaded(my_app_mod). 2> application:which_applications().{2,0,0}for the module and{2,0,0}for the application. - Validate process continuity. Inspect a long‑running process to ensure it is still alive:
The state should reflect the new code’s structure. If the process crashed, OTP would have restarted it; check the supervisor’s restart count.1> whereis(my_worker). 2> sys:get_state(whereis(my_worker)). - Check message queues. Ensure no messages are lost:
The queue length should be reasonable; a sudden spike indicates a backlog during the upgrade.1> erlang:process_info(whereis(my_worker), message_queue_len).
Expected Checks
- Module Version:
code:which_is_loaded(Module)returns the new vsn. - Application Version:
application:which_applications()shows the upgraded vsn. - Process State:
sys:get_state/1on a worker returns a term matching the new code. - No Crashes:
supervisor:which_children/1shows all children alive;supervisor:restart_count/1unchanged. - Message Queue: Length below a threshold (e.g.,
< 1000) indicates no backlog.
Recovery Options
Hot upgrades are atomic from the node’s perspective, but you still need fallback plans.
Abort the Upgrade
If the handler reports an error or you notice a problem early, abort:
1> release_handler:abort()
This rolls back to the previous code state. Verify with code:which_is_loaded/1.
Rollback to a Previous Release
Keep a copy of the old release directory. If the upgrade fails, stop the node and start the old release:
$ cd /opt/app/releases/1.5.0
$ ./bin/app start
OTP will load the old code automatically.
Restart Supervisors Manually
If a code_change fails for a particular process, OTP may terminate it. Restart the supervisor to recover:
1> supervisor:restart_child(my_sup, my_worker).
The process will be reinitialized with the new code.
Limitations & Caveats
- External API changes (e.g., new function signatures) break compatibility unless you provide a
code_changethat adapts existing state. - ETS tables and
persistent_termcontents are not migrated automatically; you must write migration logic in theappupor a separate upgrade script. - If a process does not implement
code_change/3, OTP will terminate it during the upgrade. - Large ETS tables may cause a temporary slowdown; monitor CPU and memory during the upgrade.
Practical Verification Checklist
| Check | Command | Expected Result |
|---|---|---|
| Module vsn | code:which_is_loaded(my_mod) | vsn {2,0,0} |
| App vsn | application:which_applications() | my_app {2,0,0} |
| Process alive | whereis(my_worker) | pid |
| Process state | sys:get_state(whereis(my_worker)) | new structure |
| Message queue | erlang:process_info(whereis(my_worker), message_queue_len) | < 1000 |
| Restart count | supervisor:restart_count(my_sup) | unchanged |
Conclusion
By following the steps above—ensuring proper appup files, implementing code_change/3, and verifying state post‑upgrade—you can safely perform zero‑downtime hot code upgrades in a production Erlang/OTP system. Remember to test the procedure in a staging environment first, and keep a backup of the previous release for quick rollback.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.