← All articles
TERMINAL Runtime Version Managers Guide 2026-02-09 · 9 min read · version-manager · mise · asdf

Runtime Version Managers Guide

Terminal 2026-02-09 · 9 min read version-manager mise asdf fnm nvm pyenv volta terminal devtools

Runtime Version Managers Guide

Every non-trivial development setup requires managing multiple runtime versions. Your main project needs Node 22, the legacy service needs Node 18, you're contributing to an open source project that needs Python 3.11, and your side project uses Ruby 3.3. System package managers can't handle this. You need a version manager.

The question is which one. This guide covers every version manager worth considering, from the language-specific tools to the polyglot managers that handle everything.

How Version Managers Work

Before comparing tools, it helps to understand the two fundamental approaches:

Shim-Based (asdf, mise, pyenv, rbenv)

Shim-based managers place lightweight wrapper scripts (shims) in a directory that sits early in your $PATH. When you run node, the shim intercepts the call, determines which version should be active (based on .tool-versions, .node-version, or environment variables), and delegates to the correct binary.

$ which node
/home/user/.local/share/mise/shims/node

# The shim checks .tool-versions, resolves to node 22.x, then runs:
/home/user/.local/share/mise/installs/node/22.12.0/bin/node

Pros: Works universally -- IDEs, scripts, cron jobs all pick up the right version. Cons: Shim lookup adds a small overhead to every command invocation. With asdf, this overhead is noticeable (~30-50ms) because shims are shell scripts. With mise, shims are compiled binaries and the overhead is negligible.

PATH Manipulation (fnm, nvm, Volta)

These tools modify your $PATH directly when you enter a directory (or manually switch versions). Instead of shims, the actual binary directories are prepended to your PATH.

$ fnm use 22
$ which node
/home/user/.local/share/fnm/node-versions/v22.12.0/installation/bin/node

Pros: Zero overhead per command -- node runs directly, no shim. Cons: Only works in shells that have the hook installed. Background processes, cron jobs, and some IDEs may not see the correct version.

mise (formerly rtx)

mise is the tool I recommend for most developers. It's a polyglot version manager written in Rust that's compatible with asdf plugins but dramatically faster.

Installation

# macOS
brew install mise

# Linux (official installer)
curl https://mise.run | sh

# Or via cargo
cargo install mise

Shell Setup

# Add to ~/.bashrc or ~/.zshrc
eval "$(mise activate bash)"   # for bash
eval "$(mise activate zsh)"    # for zsh
eval "$(mise activate fish)"   # for fish

Basic Usage

# Install a runtime
mise use node@22          # Install and set Node 22 for current directory
mise use [email protected]      # Install Python 3.12
mise use --global node@22 # Set global default

# List installed versions
mise ls
mise ls node

# Run a command with a specific version
mise exec node@20 -- node script.js

# Install everything defined in .tool-versions
mise install

Configuration: .tool-versions

mise reads the standard .tool-versions file (shared with asdf), plus its own .mise.toml:

# .tool-versions (asdf-compatible)
node 22.12.0
python 3.12.8
ruby 3.3.6
# .mise.toml (mise-native, more features)
[tools]
node = "22"
python = "3.12"
ruby = "3.3"

[env]
DATABASE_URL = "postgres://localhost/myapp_dev"
NODE_ENV = "development"

[tasks]
dev = "npm run dev"
test = "npm test"
lint = "npm run lint"

The .mise.toml format is more expressive -- it supports environment variables, tasks (like a Makefile), and fuzzy version matching. But .tool-versions works if you need asdf compatibility.

Why mise Over asdf

Plugin Compatibility

mise supports the entire asdf plugin ecosystem. Any asdf plugin works with mise:

mise plugin install terraform
mise use [email protected]

It also has built-in "core" plugins for popular runtimes (node, python, ruby, go, java, rust) that don't need a separate plugin install.

asdf

asdf is the original polyglot version manager. It's been around since 2014 and has a huge plugin ecosystem covering almost every runtime and tool imaginable.

Installation

# macOS
brew install asdf

# Linux (git clone)
git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.15.0

Shell Setup

# bash
echo '. "$HOME/.asdf/asdf.sh"' >> ~/.bashrc
echo '. "$HOME/.asdf/completions/asdf.bash"' >> ~/.bashrc

# zsh (with oh-my-zsh)
# Add asdf to plugins list in .zshrc
plugins=(asdf)

Basic Usage

# Add a plugin
asdf plugin add nodejs
asdf plugin add python

# Install a version
asdf install nodejs 22.12.0
asdf install python 3.12.8

# Set version for current directory
asdf local nodejs 22.12.0

# Set global default
asdf global nodejs 22.12.0

# Install all versions from .tool-versions
asdf install

When to Use asdf Over mise

Honestly, the only reason to pick asdf over mise in 2026 is if:

For new setups, mise is the better choice. It's faster, has better UX, and is fully compatible with .tool-versions files.

fnm (Fast Node Manager)

fnm is a Node.js-specific version manager written in Rust. It's extremely fast and focused on doing one thing well.

Installation

# macOS
brew install fnm

# Linux
curl -fsSL https://fnm.vercel.app/install | bash

# Windows
winget install Schniz.fnm

Shell Setup

# bash
eval "$(fnm env --use-on-cd)"

# zsh
eval "$(fnm env --use-on-cd)"

# fish
fnm env --use-on-cd | source

The --use-on-cd flag is key -- it automatically switches Node versions when you cd into a directory with a .node-version or .nvmrc file.

Basic Usage

fnm install 22
fnm use 22
fnm default 22
fnm list
fnm list-remote

Why fnm Over nvm

fnm is what nvm should have been:

When to Pick fnm

If you only need Node.js version management and nothing else, fnm is the fastest, simplest option. It's the right choice for frontend developers who don't work with Python, Ruby, or other runtimes.

nvm (Node Version Manager)

nvm is the oldest and most widely-known Node.js version manager. It's a bash script that works by manipulating your PATH.

Installation

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash

Basic Usage

nvm install 22
nvm use 22
nvm alias default 22
nvm ls
nvm ls-remote

The Elephant in the Room: Performance

nvm's biggest problem is shell startup time. Loading nvm adds 200-500ms to every new shell session. On modern hardware, this is noticeable -- especially if you open terminal tabs frequently.

There are lazy-loading hacks:

# Lazy-load nvm
export NVM_DIR="$HOME/.nvm"
nvm() {
  unset -f nvm
  [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
  nvm "$@"
}

But this means node isn't available until you first run nvm or node in that session.

Should You Still Use nvm?

If you're already using it and it works, there's no urgent reason to switch. But if you're setting up a new machine, fnm is strictly better -- it reads the same .nvmrc files, switches versions automatically, and doesn't slow down your shell.

pyenv

pyenv is the standard Python version manager. It uses the shim approach and is modeled after rbenv.

Installation

# macOS
brew install pyenv

# Linux
curl https://pyenv.run | bash

Shell Setup

# Add to ~/.bashrc
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"

Basic Usage

pyenv install 3.12.8
pyenv local 3.12.8        # Set for current directory
pyenv global 3.12.8       # Set global default
pyenv versions            # List installed
pyenv install --list      # List available

Build Dependencies

Unlike Node.js version managers that download prebuilt binaries, pyenv compiles Python from source. You need build dependencies:

# Ubuntu/Debian
sudo apt install -y make build-essential libssl-dev zlib1g-dev \
  libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm \
  libncurses5-dev libncursesw5-dev xz-utils tk-dev libffi-dev \
  liblzma-dev python3-openssl

# Fedora
sudo dnf install -y make gcc zlib-devel bzip2 bzip2-devel \
  readline-devel sqlite sqlite-devel openssl-devel tk-devel \
  libffi-devel xz-devel

This is pyenv's biggest friction point. Forgetting a dependency leads to a Python build that's missing features (like sqlite support).

pyenv vs mise for Python

mise can manage Python versions using the same underlying build mechanism as pyenv. If you already use mise for Node.js, you can add Python management without a separate tool:

mise use [email protected]

The experience is similar -- same build dependencies required, same compilation step. But you get one tool instead of two.

rbenv

rbenv is the Ruby version manager, and the inspiration for pyenv's architecture. It uses shims and a simple plugin system.

# Install
brew install rbenv ruby-build

# Setup
eval "$(rbenv init -)"

# Usage
rbenv install 3.3.6
rbenv local 3.3.6
rbenv global 3.3.6

Like pyenv, rbenv compiles Ruby from source, which requires build dependencies and takes a few minutes per version.

If you're using mise or asdf for other runtimes, there's no reason to run rbenv separately. mise/asdf handle Ruby just as well with mise use [email protected].

Volta

Volta takes a different approach to Node.js and package manager versioning. Instead of shims or PATH manipulation, Volta uses a custom binary that acts as a transparent proxy.

Installation

curl https://get.volta.sh | bash

Basic Usage

volta install node@22
volta install npm@10
volta install yarn@4

# Pin versions in package.json
volta pin node@22
volta pin npm@10

The Key Differentiator: package.json Pinning

Volta's unique feature is that version constraints are stored in package.json:

{
  "name": "my-app",
  "volta": {
    "node": "22.12.0",
    "npm": "10.9.0"
  }
}

When anyone on the team (with Volta installed) enters the project directory, they automatically get the exact Node and npm versions specified. No .nvmrc or .node-version file needed -- it's in the same package.json everyone already has.

Pros

Cons

When to Pick Volta

Volta is a good choice for teams that are exclusively JavaScript/TypeScript and want version management to be completely invisible. The package.json integration means new developers don't need to know about version managers at all -- volta install and they're done.

Team Standardization

The whole point of version managers is that your entire team uses the same runtime versions. Here's how to set this up:

The .tool-versions Approach (mise/asdf)

Create a .tool-versions file in your repo root:

node 22.12.0
python 3.12.8

Add setup instructions to your README:

## Setup
1. Install [mise](https://mise.jdx.dev): `curl https://mise.run | sh`
2. Run `mise install` in the repo root

The .node-version Approach (fnm/nvm)

Create a .node-version file:

22.12.0

Both fnm and nvm read this file. fnm switches automatically with --use-on-cd.

The package.json Approach (Volta)

{
  "volta": {
    "node": "22.12.0",
    "npm": "10.9.0"
  }
}

CI Integration

In CI, you typically don't use version managers -- you specify the runtime version directly. But you want that version to come from the same source of truth.

GitHub Actions with .tool-versions:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: jdx/mise-action@v2
      # mise reads .tool-versions and installs the right versions
      - run: node --version  # 22.12.0
      - run: python --version  # 3.12.8

GitHub Actions with .node-version:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.node-version'

Performance Comparison

Shell startup time added by each tool:

Tool Shell Startup Overhead Version Switch Speed Written In
mise <1ms <5ms Rust
fnm <1ms <5ms Rust
Volta <1ms <5ms Rust
asdf 20-50ms 30-80ms Shell script
nvm 200-500ms 50-100ms Shell script
pyenv 10-30ms 10-30ms Shell script + C
rbenv 10-30ms 10-30ms Shell script + C

The Rust-based tools are in a different league. If shell performance matters to you (and it should -- you open shells hundreds of times a day), pick a Rust-based tool.

What I'd Pick

For most developers: mise. It handles Node, Python, Ruby, Go, Java, Terraform, and hundreds of other tools through asdf plugins. One tool to manage everything, with excellent performance and DX. The .mise.toml format with environment variables and tasks makes it a genuine productivity multiplier.

For Node.js-only developers: fnm. It's the fastest, simplest Node version manager. Reads .nvmrc and .node-version files, switches automatically, no configuration needed.

For JavaScript teams that want invisible tooling: Volta. The package.json integration means developers don't even need to think about version management.

For existing asdf users: Consider migrating to mise. It reads your .tool-versions files and uses your existing asdf plugins, but it's dramatically faster. The migration is essentially: install mise, remove asdf from your shell config, add mise activation. Your .tool-versions files don't change.

Do not start new setups with nvm. fnm does everything nvm does, faster, with automatic version switching. There is no reason to choose nvm for a new setup in 2026.