Why Your Conda Solves Just Got Fast: The libmamba Shift Explained
Conda 23.3 made libmamba the default solver, cutting resolve times from minutes to seconds. Here's what changed, how to verify you're on it, and the channel-priority trap that breaks migrating workflows.
27 Feb 2026, 14:09 UTC

The Problem: Waiting Minutes for a Simple Environment
If you've ever typed conda create -n project python=3.11 numpy pandas scikit-learn and walked away for coffee, you know the classic solver's reputation. The SAT-based resolver that powered conda for years could spend five minutes contemplating version constraints before either succeeding or dumping a cryptic conflict message. For data science teams spinning up environments daily—whether locally, in CI, or inside Docker builds—that latency compounds into real productivity loss.
Conda 23.3 (April 2023) flipped the default to libmamba, a solver built on libsolv (the same engine behind openSUSE's zypper). The practical result: complex scientific stacks now resolve in seconds, error messages show conflict chains instead of opaque clauses, and channel priority behaves predictably. This post walks through what changed, how to verify you're on the new solver, and the one configuration trap that catches teams migrating from older workflows.
What Actually Changed Under the Hood
The classic solver formulated dependency resolution as a Boolean satisfiability problem. Every package version, dependency, and conflict became a clause in a massive SAT instance. While mathematically sound, this approach scales poorly when the package graph includes thousands of builds across multiple channels—exactly the situation with conda-forge plus defaults.
libmamba delegates to libsolv, which uses a directed acyclic graph with incremental propagation. Instead of exploring the entire solution space, it propagates constraints forward, backtracks only when forced, and caches learned clauses. For a typical numpy + pandas + scikit-learn stack, solve time drops from ~180 seconds to ~12 seconds on a cold cache. Warm solves (reusing the index) are sub-second.
Two behavioral shifts matter for daily work:
- Strict channel priority is now default. If your
.condarclistsconda-forgeabovedefaults, libmamba will never silently fall back to adefaultsbuild to satisfy a dependency. This prevents the subtle ABI mismatches (MKL vs OpenBLAS, different glibc linkages) that previously surfaced as runtime crashes. - Conflict reporting shows chains. Instead of "UnsatisfiableError: The following specifications were found to be incompatible," you get a readable trace:
package A requires B>=2.0, but package C requires B<2.0. You can actually fix the pin rather than guessing.
Worked Example: From environment.yml to Reproducible Lockfile
Suppose you maintain a project with this environment.yml:
name: ml-project
channels:
- conda-forge
- nodefaults
dependencies:
- python=3.11
- numpy
- pandas
- scikit-learn
- pip
- pip:
- torch==2.1.0
With the classic solver, conda env create -f environment.yml might pull different transitive dependency versions on Linux vs macOS because channel fallback order wasn't strictly enforced. libmamba + --from-history fixes this.
First, verify your solver:
conda info | grep solver
# solver: libmamba
If it shows classic, upgrade: conda update -n base conda (requires conda ≥ 23.3).
Now create the environment and export only user-requested specs:
conda env create -f environment.yml
conda activate ml-project
conda env export --from-history > environment.lock.yml
The resulting environment.lock.yml contains only the top-level packages you asked for. Hand this file to a colleague on Windows or to your CI runner; libmamba will reconstruct the exact same transitive closure deterministically because channel priority is strict and the index is versioned.
For CI/CD where you need fully pinned, platform-specific lockfiles, add conda-lock:
pip install conda-lock
conda-lock lock -f environment.yml --kind explicit
# produces conda-lock.yml with linux-64, osx-64, osx-arm64, win-64 entries
Commit conda-lock.yml. Your GitHub Actions workflow then runs:
- name: Create env from lockfile
run: conda create -n ci-env --file conda-lock.yml
No solve step in CI—just a transaction. Bit-for-bit reproducibility across runners.
The Migration Trap: Channel Order Matters Now
The most common breakage after upgrading isn't a bug—it's the stricter channel priority finally doing what it always promised. Teams that relied on implicit fallback (e.g., a package only on defaults satisfying a dependency while conda-forge was first in .condarc) will see "Package not found" errors.
Fix: make channel order explicit per environment. In your project's .condarc (committed to repo):
channel_priority: strict
channels:
- conda-forge
- nodefaults # or keep defaults if you have a token
Or configure per-environment without a file:
conda config --env --add channels conda-forge
conda config --env --set channel_priority strict
If you genuinely need a package only available on defaults (and have a commercial token), add it as a lower-priority channel. Do not disable strict priority; it defeats the ABI safety net.
Docker and CI: Smaller Images, Faster Builds
The old guidance "use continuumio/anaconda3 for convenience" produces 3 GB+ images. With libmamba, the minimal base is:
FROM continuumio/miniconda3
RUN conda update -n base conda \
&& conda create -n app python=3.11 numpy pandas -y
# image size ~1.1 GB
For even smaller footprints, consider mambaforge (pre-bundled libmamba) or micromamba (static binary, no base environment). The solver performance is identical; the difference is startup overhead.
Limitations to Know Before You Commit
- Python 3.7 support ended in conda 24.x. Environments pinned to 3.7 must stay on conda 23.x or migrate Python version first.
defaultschannel requires a paid token for commercial use since 2024.conda-forgeremains free but builds with OpenBLAS instead of MKL—numerical results can differ slightly. Test your workloads.conda install --dry-runoutput differs. CI scripts that parse classic solver dry-run text will need updating. Useconda create --dry-runwith libmamba for structured JSON output (--jsonflag).- Windows long-path issues persist on conda < 23.10. Enable
conda config --set enable_long_paths trueor upgrade. - Anaconda Navigator still uses classic logic for some GUI operations. CLI and Navigator may disagree in edge cases. Prefer CLI for reproducibility.
Actionable Next Steps
- Run
conda info | grep solver. If it saysclassic, upgrade:conda update -n base conda. - Audit your
.condarcor per-environment channel config. Ensurechannel_priority: strictand explicit channel order. - Replace
conda env exportwithconda env export --from-historyin your onboarding docs. - Add
conda-lockto your CI pipeline for fully pinned, multi-platform lockfiles. - Switch Docker bases from
anaconda3tominiconda3+ libmamba; verify image size drops below 1.5 GB.
The solver swap is one of those rare changes that's purely additive: faster, clearer, and more reproducible without rewriting your environment files. The only work is aligning your channel configuration to match the stricter defaults—and that alignment prevents the very class of subtle bugs that used to waste weekends.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.