Skip to content
Latchkey

Elixir OTP version mismatch ".beam file was compiled" in CI

BEAM bytecode is tied to the OTP major it was compiled under. When a restored _build cache was built with a different OTP than the runner now has, the VM refuses the .beam files and compilation breaks.

What this error means

After bumping otp-version (or a cache from another job), the build fails loading modules that "were compiled for a different version" of Erlang/OTP.

** (Mix)
** (exit) :badfile
module was compiled for a different version of the Erlang runtime system

Common causes

Restored _build cache built under a different OTP

actions/cache restored _build compiled with an older OTP major; the new runner OTP cannot load those beams.

Cache key does not include the OTP/Elixir version

A key keyed only on mix.lock reuses artifacts across toolchain bumps, so stale beams leak into the new environment.

How to fix it

Key the cache on OTP and Elixir versions

  1. Include the OTP and Elixir versions (and MIX_ENV) in the cache key.
  2. Bumping either version then produces a fresh cache instead of reusing stale beams.
  3. Cache deps and _build separately so a rebuild is cheap.
.github/workflows/ci.yml
- uses: actions/cache@v4
  with:
    path: |
      deps
      _build
    key: mix-${{ runner.os }}-otp${{ steps.beam.outputs.otp-version }}-elixir${{ steps.beam.outputs.elixir-version }}-${{ hashFiles('**/mix.lock') }}

Force a clean compile when in doubt

Clear the build directory so nothing stale from another OTP is loaded.

Terminal
mix deps.clean --all
mix clean
mix deps.get && mix compile

How to prevent it

  • Put OTP and Elixir versions in the _build cache key.
  • Give setup-beam an id and read its version outputs into the key.
  • Rebuild from clean when bumping the toolchain.

Related guides

Run this faster and cheaper on Latchkey managed runners. Start free →