Skip to content
Latchkey
Published June 2026 by Daniel Zoghalchali

The State of Elixir CI 2026

Fast tests, slow checks: mix deps caching, the BEAM build, ExUnit speed, and the Dialyzer plus Credo tax that flips the usual CI shape.

47%
of Elixir CI wall-clock spent compiling deps and the project, not testing
Latchkey analysis (modeled)
6.1x
cold Dialyzer PLT build vs incremental on a cached PLT
Latchkey analysis (modeled)
38s
median cold mix deps.get plus compile for a Phoenix app
Latchkey analysis (modeled)

Executive summary

Elixir flips the usual CI shape. In most ecosystems the test run is the long pole, so optimization effort goes into parallelizing and sharding tests. ExUnit is genuinely fast thanks to async tests running concurrently on the BEAM, so for Elixir teams the test phase is rarely the bottleneck. The cost moves upstream, into compilation and quality checks, which is where this report spends most of its attention.

With CI/CD adoption at 76% among professional developers, the Elixir question in 2026 is why a language with fast tests still ends up with a slow pipeline. The answer is that the most expensive steps, fetching and compiling dependencies, building the project, and running the two slowest checks in the ecosystem, Dialyzer and Credo, are rebuilt cold on ephemeral runners that start clean every push. The work is fast in principle and slow in practice purely because it is repeated from scratch.

Almost all of this is repeated, unchanged work: the same dependencies, the same compiled BEAM artifacts, the same Dialyzer persistent lookup table. None of it changes between most pushes, yet a cold runner recomputes all of it every time. Persisting these artifacts across runners is what turns a cold Elixir pipeline into a warm one, and it is the single highest-leverage intervention available to an Elixir team.

This report quantifies where the Elixir CI minute actually goes, why the Dialyzer PLT is the most expensive cold build in a typical pipeline, where mix deps caching is easy to get subtly wrong, and how transient hex.pm and compile flakes tax an otherwise fast suite out of proportion to their frequency. The throughline matches the broader CI picture: the waste is mechanical and cacheable rather than fundamental, so it is addressable without changing a line of application code.

For Elixir teams the practical message is that the BEAM is not the problem. A correctly warmed pipeline restores its deps, its compiled output, and its PLT instead of rebuilding them, runs its fast async tests, and self-heals the occasional hex.pm blip. The teams that do this run Elixir CI that is as fast as the language's reputation suggests, while teams on cold pipelines pay repeatedly for work the cache should have eliminated.

Where an Elixir CI minute goes
Deps + project compile 38%
Dialyzer 24%
ExUnit 22%
Credo, format, setup 16%

Modeled split of billed minutes for a Phoenix pipeline. · Source: Latchkey analysis (modeled)

Dialyzer time, cold PLT vs cached PLT
Cold PLT build214 sGitHub cache88 sWarm incremental PLT35 sManaged cache28 s

Time to run Dialyzer by PLT cache state. · Source: Latchkey analysis (modeled)

Email me the report

The full report is right here on this page, free. Want the link in your inbox to read later or share, plus new Latchkey reports as they drop? Drop your email and we will send it over.

Sent! Check your inbox for the report link.

No spam. Unsubscribe anytime.

Elixir's bottleneck is compilation, not the test run

ExUnit's async tests run concurrently on the BEAM, so unlike most ecosystems the test phase is rarely the long pole in an Elixir pipeline. Our modeling puts deps-and-project compilation well ahead of the test run as a share of the billed minute, which is the inverse of the usual CI shape where test sharding is the dominant optimization.

The cost lives in compiling dependencies and the project, which on a cold runner happens from scratch on every push. The BEAM compile is not slow in absolute terms, it is slow because it is repeated: the same source compiles to the same artifacts run after run, and an ephemeral runner that starts clean throws those artifacts away and rebuilds them each time.

Caching the compiled BEAM artifacts across runs is where the dominant Elixir CI cost actually gets reclaimed. Because the test run is already fast, parallelizing tests buys an Elixir team little; the leverage is upstream, in never recompiling unchanged code. The donut below shows compilation as the largest single slice of the Elixir minute, which is the slice a warm build cache removes.

The Dialyzer PLT is the single most expensive cold build

Building the Dialyzer persistent lookup table from scratch is by far the slowest step in a typical Elixir pipeline. The PLT captures type information for the entire dependency tree and the OTP standard library, and assembling it cold is a multi-minute operation that dwarfs every other step when it runs without a cache.

On ephemeral runners the PLT is rebuilt cold every time, which is close to pure waste because the PLT changes rarely. It is a function of the dependency set and the Elixir and OTP versions, none of which move on a typical push, so the cold rebuild recomputes an artifact that was already valid from the previous run.

A persistent managed cache for the PLT turns a multi-minute cold build into a short incremental check, our modeling shows the cached and incremental cases running several times faster than the cold build. Because the PLT is both the most expensive step and one of the most stable artifacts, it is the highest-return single thing an Elixir team can cache. The chart below shows the gap between a cold PLT build and a warm incremental one.

  • The PLT is a function of the dependency set plus the Elixir and OTP versions, so it changes rarely.
  • Rebuilding it cold on every push recomputes an artifact that was already valid.
  • A persistent PLT cache turns a multi-minute cold build into a short incremental check.

mix deps caching is easy to get subtly wrong

Caching deps correctly means caching two distinct things: the fetched dependency source and the compiled _build artifacts. Teams that cache only the fetched source still pay a full cold compile, because the source is present but the BEAM output is not, so the savings are a fraction of what a correct cache delivers.

Both caches have to be keyed correctly, on the lockfile and on the Elixir and OTP versions, or they serve stale or mismatched artifacts. A cache keyed too loosely restores compiled output that does not match the current toolchain, which can produce confusing failures; a cache keyed too tightly almost never hits and silently degrades to a cold build. This fragility is why so many teams have caching configured and still pay cold-compile prices.

A managed cache shared across the fleet keeps both the source and the compiled output warm without the per-run cache-key fragility the GitHub cache invites. Because the cache is fleet-wide rather than per-run, the first job on a branch benefits from work a previous job already did, which is exactly the case where the GitHub cache cold-misses. The chart below shows how much faster a correctly warmed deps step runs than a cold one.

mix deps.get plus compile, cold vs warm
Cold, no cache38 sGitHub cache19 sWarm local cache9 sManaged cache4 s

Dependency fetch and BEAM compile time per run. · Source: Latchkey analysis (modeled)

Credo and hex.pm flakes still tax an otherwise fast suite

Credo adds a fixed static-analysis cost on every run. It is not slow in isolation, but because the rest of the Elixir pipeline is fast, a fixed per-run cost is a proportionally larger slice than it would be in a slower ecosystem, and it runs on every push whether or not the changed code touches anything Credo cares about.

Transient hex.pm fetch failures and compile-time deadlocks fail builds that pass cleanly on retry. A registry blip during mix deps.get, or a rare deadlock in parallel compilation, produces a red build that has nothing to do with the code under test. Because Elixir pipelines are fast, these spurious failures are a disproportionate share of the wasted re-runs an Elixir team sees.

Self-healing runners absorb the transient failure rather than failing the whole pipeline, retrying the affected step on a fresh environment when the failure matches a known-transient signal. This keeps a hex.pm hiccup from turning into a red check on a pull request and lets the team hold change-failure rate in the elite 0-15% band without re-running jobs by hand.

Async ExUnit is an asset the pipeline should preserve

ExUnit's async tests are one of Elixir's genuine CI advantages, and the pipeline should be built to preserve that speed rather than undermine it. The most common way teams accidentally slow their fast suite is by forcing tests synchronous to work around shared-state contention, particularly around the database sandbox, which serializes work that the BEAM could have run concurrently.

Keeping tests async requires disciplined isolation: each async test owns its own data and its own connection from the Ecto sandbox, so tests do not contend on shared rows or global state. When that discipline holds, the BEAM's concurrency means adding tests barely moves wall-clock, which is why Elixir suites scale so gracefully compared to ecosystems where every test is a fresh process.

The runner layer supports this by providing clean, isolated environments so that flakiness from state bleeding between runs does not push teams toward defensive synchronous tests. A fresh environment per job removes the cross-run contamination that makes async tests appear flaky, which keeps the fast async path the default rather than something teams retreat from after a confusing failure.

The Elixir minute is cheap Linux compute, so the cache is the whole game

Elixir CI is almost entirely Linux compile work. There is no macOS or Windows premium to fight as there is in mobile or .NET pipelines, which means the per-minute rate is already the cheap end of the runner spectrum. That changes where the savings come from: for Elixir, the lever is the minute count, not the per-minute rate.

Because the base rate is low, the dominant cost driver is how many minutes the pipeline burns, and that count is set almost entirely by cache state. A cold pipeline that rebuilds deps, compiled output, and the PLT every push burns many times the minutes of a warm one running identical code, so the cache state is effectively the cost.

This makes Elixir a case where caching is not one optimization among several, it is the optimization. The runner-cost chart below shows the modest per-minute rate for Linux compute, which reinforces the point: with the rate already low, the only large lever left is to stop billing minutes for work the cache should have eliminated.

Hosted runner cost per minute by platform
Linux 2-core$0.008Windows 2-core$0.016macOS$0.08Managed (Latchkey)$0.0025

Published GitHub-hosted rates vs a managed alternative. · Source: GitHub Actions pricing + Latchkey rates

Managed runners change the Elixir CI break-even

Because Elixir CI is cache-sensitive Linux compile work, the win comes from never rebuilding the deps and PLT cold rather than from chasing a cheaper operating system. Managed runners capture roughly 70% of the hosted cost with zero ops, which lowers the rate, but the larger effect is what they do to the minute count.

By persisting the compile and PLT caches across the fleet, a managed layer cuts the minute count on top of the per-minute rate. The two savings stack: a lower rate applied to fewer minutes, because the warm cache removes the cold compile and the cold PLT build that would otherwise dominate the bill. For an Elixir team this is the rare case where the caching benefit clearly outweighs the rate benefit.

On top of the cost, the managed layer self-heals transient hex.pm and compile flakes and provides the clean per-job isolation that keeps async ExUnit fast. The cost story, the reliability story, and the test-isolation story converge on the same architecture, which is what makes warm managed runners the durable answer for Elixir CI rather than a marginal tweak.

  • Managed runners capture roughly 70% of hosted cost with zero ops.
  • Persistent fleet-wide caches cut the minute count, the larger lever for low-rate Linux work.
  • Per-job isolation keeps async ExUnit fast and self-heals hex.pm and compile flakes.

Recommendations

Cache the PLT first, it is the biggest single win

The Dialyzer PLT is both the most expensive step and one of the most stable artifacts in an Elixir pipeline, so persisting it across runs is the highest-return single action available. Key it on the dependency set and the Elixir and OTP versions, and confirm it restores rather than rebuilds on a typical push, turning a multi-minute cold build into a short incremental check.

Cache both deps source and compiled _build output

Caching only the fetched dependency source leaves a full cold compile on the table. Persist both the source and the compiled _build artifacts, keyed on the lockfile and the toolchain versions, so a warm run restores the BEAM output instead of recompiling it. Measure the hit rate so a silently cold-missing cache does not keep charging you for compilation.

Keep ExUnit async and isolated

Preserve Elixir's fast async test path by giving each async test its own data and its own Ecto sandbox connection, so tests do not contend on shared state and get forced synchronous. Run tests on clean isolated environments so cross-run contamination does not make async tests appear flaky and push the team toward slower synchronous workarounds.

Treat the cache hit rate as a tracked metric

For Elixir, where the base per-minute rate is already low, the minute count is set almost entirely by cache state, so the cache hit rate is effectively a cost metric. Track it the way you track test coverage, and alert when it regresses, because a cache that silently stops hitting is a pipeline that has quietly gone cold and expensive.

Auto-heal hex.pm and compile flakes instead of re-running by hand

Transient hex.pm fetch failures and rare parallel-compile deadlocks are mechanical and pass on a clean retry. Retrying the affected step on a fresh environment removes these spurious red builds without touching code, which matters disproportionately for Elixir because the fast pipeline makes each spurious failure a large share of total wasted re-runs.

Outlook

Expect the cache story to remain the center of gravity for Elixir CI through 2026. As Phoenix applications grow and dependency trees deepen, the gap between a cold pipeline that rebuilds the PLT and the BEAM output every push and a warm one that restores them widens further, because the cold cost scales with the dependency set while the warm cost stays nearly flat. The teams that have solved persistent caching will keep extending their speed advantage as their codebases grow.

Incremental tooling in the ecosystem is moving in the same direction. As incremental Dialyzer and smarter compilation caching mature, the warm-pipeline numbers should improve further, but only for teams whose runner layer actually persists the relevant artifacts across runs. The benefit of incremental tooling is gated on the cache surviving, which keeps the runner layer central to realizing it.

For most Elixir teams the practical takeaway is that the BEAM lives up to its fast reputation when the pipeline is warm, and falls short of it only when the pipeline is cold. A persistent PLT cache, a correct two-part deps cache, disciplined async tests, and a runner layer that heals transient hex.pm failures turn Elixir CI into the fast pipeline the language promises, without changing a line of application code.

Methodology

This report combines public Elixir ecosystem signals (Hex package usage, Phoenix release cadence, published runner pricing) with Latchkey runner analysis of mix, ExUnit, Dialyzer, and Credo pipelines. Modeled figures reflect a representative Phoenix application with a cached PLT workflow and are illustrative estimates intended to show direction and magnitude rather than precise population values; the CI adoption headline is from the Stack Overflow Developer Survey. Figures labeled "modeled" are illustrative estimates derived from public pricing and typical pipeline shapes, not a primary survey; figures attributed to a named source reflect that source. Pricing reflects published rates at time of writing and should be verified against current provider pricing.

Sources

More Latchkey reports

See what you would save with Latchkey managed runners and self-healing. Start free → 30-day trial · No credit card