Diagnosing Gleam Incremental Build Cache Misses and Stale Artifacts
Gleam says "Up to date" after you just edited a file, or the runtime crashes on a module you renamed. A diagnostic map from symptom to cause — mtime granularity, orphaned .beam files, dependency graph gaps, cross-target cache collisions, and CI cache poisoning — with the exact checks and fixes.
23 Aug 2026, 11:05 UTC

You edited a Gleam module, ran gleam build, saw "Up to date", and your change never made it into the running program. Or worse: the build succeeded but the Erlang runtime crashed with module not found because an old .beam file is still sitting in the build directory. These are incremental-compilation cache problems, and they have a small set of recognizable causes. This guide maps each symptom to a diagnostic check and a fix, for Gleam 1.0–1.6 on Erlang/OTP 25–27.
Symptom-to-cause quick reference
| Symptom | Likely cause | First check |
|---|---|---|
| "Up to date" right after saving an edit | File-system mtime granularity hides the change | Compare source and artifact mtimes with stat |
Runtime module not found after renaming a module | Orphaned .beam from the old name still in build/ | List build/dev/ebin/ for stale names |
| New dependency added, dependent modules not recompiled | Dependency graph edge not visible to the incremental tracker | Check build/manifest.toml vs. recompile output |
| Wrong artifacts after switching Erlang/JavaScript targets | Build output not cleanly namespaced per target | Check file extensions under build/dev/ |
| Tests pass locally, fail in CI with missing modules | CI restored a partial or stale build/ cache | Look for "Up to date" in CI logs for modules that should compile |
Check 1: Rule out mtime granularity
Gleam's incremental builder decides what to recompile largely from file modification times. Many file systems store mtimes at coarse granularity — around 1 second on ext4 and up to 2 seconds on FAT; network mounts (NFS, SMB) can add timestamp skew on top of that. Two writes inside one granularity window can look identical to the tracker, so an edit made immediately after a build is invisible.
Run this in your project root (no special permissions needed):
stat src/my_module.gleam build/dev/ebin/my_module.beamCompare the Modify timestamps. If the source's mtime is not strictly newer than the artifact's — or they match within a second — the tracker had no way to see your edit. The fix is trivial: wait a couple of seconds and touch src/my_module.gleam, then rebuild. If you hit this constantly (common on macOS with rapid save loops or on network-mounted home directories), make "touch then build" a habit, or script your editor's save hook to sleep briefly.
Check 2: Hunt for orphaned artifacts after renames
Renaming or moving a module removes the old source file, but the old compiled artifact can linger under build/. The compiler produces a fresh .beam for the new name while the stale one remains loadable, which produces confusing runtime failures — stale function clauses, or module not found when something still references the old name.
Diagnose by listing the output directory and diffing against your source tree:
ls build/dev/ebin/*.beam | sed 's/.*\///; s/\.beam//' | sort > /tmp/artifacts.txt
ls src/*.gleam | sed 's/.*\///; s/\.gleam//' | sort > /tmp/sources.txt
diff /tmp/sources.txt /tmp/artifacts.txtAny artifact name with no matching source is a suspect. The fix is a full reset of the build directory:
gleam clean && gleam buildThis deletes compiled output, so the next build takes longer — on a large project that can mean tens of seconds to a couple of minutes. It changes no source state, so there is nothing to roll back.
Check 3: Verify dependency changes actually propagated
After adding a dependency to gleam.toml, run gleam deps download and then gleam build. Confirm the new package appears in build/manifest.toml. If the manifest updated but no dependent module recompiled, the incremental tracker missed the graph edge — this is most likely with transitive dependencies pulled in through Erlang/OTP applications rather than direct Gleam imports, which the tracker does not always observe.
The targeted fix is a forced full recompile:
gleam build --rebuild--rebuild discards all incremental state, so reserve it for cases like this rather than making it your default. On codebases past a few hundred modules the overhead is noticeable.
Check 4: Separate cross-target builds
If you build for both Erlang and JavaScript, inspect what each target actually produced:
gleam build --target javascript
ls build/dev/javascript/ # expect .mjs output
gleam build --target erlang
ls build/dev/ebin/ # expect .beam outputIf you find artifacts from the wrong target mixed in, or a target switch left stale files behind, the safest workflow is to run gleam clean when switching targets, or keep the two builds in separate working copies or CI jobs. Verify by checking that the file extensions in each output directory match the target you just built.
Check 5: Audit the CI cache
A classic failure: CI restores a cached build/ directory, the tracker reports "Up to date" for test modules, and the test run then fails with module not found because the cache was saved from a different commit or without the corresponding sources. Search your CI logs for "Up to date" on modules that changed in the commit being built — that mismatch is the smoking gun.
Two workable fixes: cache only the downloaded dependencies (the packages directory) rather than the entire build/ tree, or run gleam clean at the start of every CI job. The first keeps dependency downloads fast while forcing honest recompilation; the second is simpler and costs a full compile each run.
When to escalate
Three situations call for more than a clean build. First, an Erlang/OTP major version upgrade: .beam compatibility is not guaranteed across majors, so always gleam clean after switching OTP versions — treat it as mandatory, not diagnostic. Second, a Gleam toolchain upgrade: caching internals may change between releases, so check gleam --version and read the changelog for build-system notes before trusting old assumptions; the mtime-based behavior described here reflects the 1.x line up to 1.6 and should be re-verified on newer releases. Third, if a clean rebuild still produces stale behavior, the problem is almost certainly not the cache — look at code-loading paths, release assembly, or a stale _build in an embedded Erlang dependency instead.
A five-minute reproduction to confirm the diagnosis
To see the mtime issue yourself: gleam new demo && cd demo && gleam build, then edit src/demo.gleam and immediately rebuild. If you see "Up to date", run stat src/demo.gleam against the artifact, wait two seconds, touch the source, and rebuild — the recompile confirms the granularity hypothesis. Do not treat a single run as proof on network file systems; repeat with a deliberate sleep to isolate timing from other causes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.