If you’ve searched the Frappe forum for a “standard” way to upgrade ERPNext from v15 to v16, you’ve probably noticed the same thing I did: there isn’t one. Every guide assumes a slightly different OS, a different Python setup, and a different set of installed apps. After performing this upgrade a number of times on different servers, I’ve consolidated what consistently works into one guide – including the errors you’re most likely to hit and how to get past them.
This is not a five-minute upgrade. Version 16 changes the runtime requirements of the framework itself (new Python, new Node), which means you’re not just switching a git branch – you’re rebuilding the foundation your bench runs on. Plan for downtime, and plan for a rollback.
Part 1: What Must Be in Place Before You Start
Do not run a single upgrade command until every item below is true. Most failed upgrades I’ve seen were doomed before bench switch-to-branch was ever typed.
1. A healthy, up-to-date v15 installation
Your site must be on the latest v15 minor release and working correctly. Run:
bench version
bench --site yoursite.com migrate
If bench migrate fails on v15, fix that first. Never upgrade a broken bench. Also: don’t skip versions. If you’re on v14, go to v15 first, verify everything, and only then attempt v16.
2. The new runtime requirements
This is the single biggest difference from previous upgrades. Frappe/ERPNext v16 requires:
| Component | v15 typical | v16 requirement |
|---|---|---|
| Python | 3.10 – 3.11 | 3.14.x |
| Node.js | 18 / 20 | 24 |
| OS | Ubuntu 22.04 / Debian 11+ | Ubuntu 24.04+ / Debian 12–13 recommended |
| MariaDB | 10.6+ | 10.6+ (unchanged, but be current) |
| Redis | 6+ | Current stable recommended |
Frappe v16’s codebase uses Python 3.12+ syntax (e.g. type X = ... type aliases), so running it on Python 3.11 produces a hard SyntaxError – it won’t even import. Node 22 and below will fail during asset builds. Verify what you have:
python3 --version
node --version
mariadb --version
redis-server --version
If your server is on Ubuntu 22.04 or Debian 11, seriously consider whether you’d rather migrate to a fresh server on Ubuntu 24.04 instead of upgrading in place. Installing Python 3.14 on an older OS via pyenv works, but you’re maintaining a non-standard setup forever after.
3. A backup you have actually tested
Take three layers of protection:
# Full site backup with files
bench --site yoursite.com backup --with-files
Copy the backup files off the server. Then, if you’re on a VM or cloud instance, take a full snapshot of the machine. Finally – and this is the step everyone skips – restore the database backup on a scratch server or container and confirm it actually loads. A backup you’ve never restored is a hope, not a backup.
4. Every custom app must have a version-16 branch (or be removed)
List your apps:
bench version --format table
ls ~/frappe-bench/apps
For each third-party or custom app, check whether a version-16 branch exists and whether the maintainer has declared v16 compatibility. Apps written for v15 frequently use deprecated APIs that break under v16.
Critically, some apps that worked on v15 simply don’t have v16 support yet. In my experience (and confirmed widely on the forum), apps like telephony, raven, drive, insights, and various WhatsApp integrations were common blockers in the early v16 days. Uninstall or remove unsupported apps from your v15 bench before upgrading – leaving them in place causes ModuleNotFoundError crashes mid-migration, which is the worst possible time to discover the problem.
bench --site yoursite.com uninstall-app appname
bench remove-app appname
5. A staging run
Restore your production backup onto a staging server (or a fresh v16 bench) and rehearse the entire upgrade there first. This is where you discover that one custom script using a removed API, with zero business consequences. Only after a clean staging run should you touch production.
6. Housekeeping
A few small things that prevent big headaches: at least 2x your database size in free disk space, a maintenance window communicated to users, root/sudo access confirmed, and git status clean in every app directory (uncommitted local changes will block the branch switch).
Part 2: The Upgrade, Step by Step
The commands below assume a production bench at ~/frappe-bench owned by a frappe user, managed by supervisor and nginx.
Step 1 – Enable maintenance mode and stop services
cd ~/frappe-bench
bench --site yoursite.com set-maintenance-mode on
sudo supervisorctl stop all
sudo systemctl stop nginx
Step 2 – Install Node 24
Install it system-wide so supervisor can find it (nvm-only installs cause supervisor spawn errors later, because supervisor doesn’t load your shell profile):
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt-get install -y nodejs
node --version # must show v24.x
Step 3 – Install Python 3.14
On Ubuntu 24.04 you may be able to get it from the deadsnakes PPA or system packages; on Debian 12 and most other distros, pyenv is the reliable path:
# Build dependencies
sudo apt update
sudo apt install -y build-essential libssl-dev zlib1g-dev libbz2-dev \
libreadline-dev libsqlite3-dev libncursesw5-dev xz-utils tk-dev \
libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev curl git
# pyenv
curl https://pyenv.run | bash
Add to ~/.bashrc:
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
Then:
exec $SHELL
pyenv install 3.14.2
pyenv global 3.14.2
python --version # must show 3.14.x
Step 4 – Rebuild the bench virtualenv on Python 3.14
This is the step most guides disagree on. The key fact: bench always uses ~/frappe-bench/env, not whatever virtualenv you happen to have activated in your shell. That internal env must be rebuilt on Python 3.14.
The clean built-in way:
cd ~/frappe-bench
bench migrate-env $(pyenv which python)
If migrate-env misbehaves (it sometimes does when the old env is broken), the manual route works:
cd ~/frappe-bench
rm -rf env
python -m venv env
source env/bin/activate
pip install -U pip wheel setuptools
pip install -U frappe-bench
pip install -e apps/frappe
pip install -e apps/erpnext
pip install -e apps/hrms # if installed
Don’t panic about dependency-resolution warnings at this stage – requirements get properly re-synced after the branch switch.
Step 5 – Switch every app to version-16
cd ~/frappe-bench
bench switch-to-branch version-16 frappe --upgrade
bench switch-to-branch version-16 erpnext --upgrade
bench switch-to-branch version-16 hrms --upgrade
# ...and any custom apps that have a version-16 branch
The ERPNext switch can take a long time (15+ minutes is normal on modest servers) — let it run. Then:
bench update --patch
Step 6 – Requirements, build, migrate
bench update --requirements
bench build
bench --site yoursite.com migrate
bench migrate is the moment of truth: it runs every database patch between v15 and v16. Watch the output. If it fails, read the traceback carefully – the fix is almost always in Part 3 below.
Step 7 – Regenerate configs and restart
Because the env and Node paths changed, regenerate supervisor config:
bench setup supervisor
sudo supervisorctl reread
sudo supervisorctl update
bench setup nginx
sudo supervisorctl restart all
sudo systemctl restart nginx
bench --site yoursite.com set-maintenance-mode off
Step 8 – Verify
bench version # should show frappe 16.x, erpnext 16.x
sudo supervisorctl status
Log in, then walk through a real workflow: create a draft Sales Invoice, run a stock report, check a print format, send a test email, and confirm scheduled jobs are running (bench --site yoursite.com doctor). Keep an eye on the Error Log doctype for the first 24–48 hours.
Part 3: Common Errors and Their Solutions
These are the failures that come up again and again, with what actually fixes them.
SyntaxError on lines like type X = ...
Cause: Frappe v16 code is running on Python ≤ 3.11. The framework uses modern type-alias syntax that older interpreters can’t parse. Fix: Install Python 3.14 and rebuild ~/frappe-bench/env on it (Steps 3–4). Confirm with ~/frappe-bench/env/bin/python --version – checking your shell’s python3 is not enough; the env interpreter is what matters.
ERROR: Package 'frappe' requires a different Python: 3.14.x not in '<3.14,>=3.10'
Cause: You’re installing into an env that still points at a v15 checkout of frappe (whose metadata pins Python below 3.14), or pip is reading stale build metadata. Fix: Make sure apps/frappe is actually on version-16 (git -C apps/frappe rev-parse --abbrev-ref HEAD), then delete and recreate the env from scratch. The v16 branch requires Python >=3.14,<3.15, so 3.14.x is correct – this error means your checkout or env state is stale, not your Python.
ModuleNotFoundError: No module named 'telephony' (or raven, drive, etc.)
Cause: An app installed on v15 has no v16-compatible code present after the switch, but it’s still registered in apps.txt and the site’s installed apps. Often accompanied by “Backup failed… Database or site_config.json may be corrupted” – the config usually isn’t corrupted; bench just crashes while trying to load the missing module. Fix: The clean approach is to roll back to your snapshot, uninstall/remove the incompatible apps on v15 while everything still works, verify the site, then redo the upgrade. Reinstall those apps later once they ship v16 support. If you can’t roll back, remove the app from ~/frappe-bench/sites/apps.txt and the bench, run bench --site yoursite.com remove-from-installed-apps appname, then retry.
Socketio: ECONNREFUSED 127.0.0.1:11000 / supervisor “spawn error” / Service redis_cache is not running
Cause: The Node socketio process starts before bench’s Redis processes are up, or supervisor can’t find Node 24 (typically because Node was installed only via nvm). Fix: Start things in the right order:
sudo supervisorctl start frappe-bench-redis:
sudo supervisorctl start frappe-bench-workers:
sudo supervisorctl restart frappe-bench-web:frappe-bench-node-socketio
If socketio still won’t spawn, install Node 24 system-wide (Step 2) and run bench setup supervisor again so the config points at the right binary.
bench update fails restarting workers: ERROR (no such file)
Cause: After the branch switch, supervisor group names in the generated config no longer match what’s loaded, so bench tries to restart a group that doesn’t exist. Fix: Regenerate and reload supervisor config:
bench setup supervisor
sudo supervisorctl reread && sudo supervisorctl update
sudo supervisorctl restart all
bench migrate fails on a patch with a database error
Cause: Varies – corrupted tables, a custom field colliding with a new standard field, or leftover data from an old app. Fix: Read the traceback to identify the patch and table. Check table integrity from bench --site yoursite.com mariadb (CHECK TABLE, REPAIR TABLE where appropriate). If a custom field collides with a new v16 standard field, rename or delete the custom field and re-run. bench migrate --skip-failing exists, but treat it as a diagnostic tool, not a solution – anything skipped must be resolved before go-live.
Asset build fails or the UI looks broken after upgrade
Cause: Assets built with an old Node, or stale cached assets. v16 ships a substantially redesigned desk, so a half-built bundle is very visible. Fix:
bench build --force
bench --site yoursite.com clear-cache
bench --site yoursite.com clear-website-cache
Then hard-refresh the browser (Ctrl+Shift+R). Confirm node --version shows 24 in the same shell context supervisor uses.
Customizations or client scripts stopped working
Cause: v16 removes and renames APIs that were deprecated in v15; custom scripts calling them fail silently or throw in the console. Fix: Check the browser console and the Error Log doctype. Update scripts to the new APIs – this is exactly what your staging rehearsal was for. Note also that some functionality moved out of core (for example, remote/S3 backups now require installing a separate app), so “missing” features may just need an extra bench get-app.
Multi-tenant bench: second site fails after the first migrates fine
Cause: Each site migrates independently; a site-specific app list or stale config on the second site trips the same issues above. Fix: Run bench --site secondsite.com migrate individually, read its specific error, and apply the relevant fix per site. Verify each site’s site_config.json and installed-apps list separately.
Part 4: If It All Goes Wrong – Rollback
This is why you took the snapshot. The fastest rollback is restoring the VM snapshot taken in Part 1. If you only have database backups, the manual path is:
bench switch-to-branch version-15 frappe --upgrade
bench switch-to-branch version-15 erpnext --upgrade
bench setup requirements
bench --site yoursite.com restore /path/to/v15-backup.sql.gz
bench restart
Remember you’ll also need to point the bench env back at your old Python version if you rebuilt it. This is messy enough that the snapshot route is strongly preferred – which is the whole argument for taking one.
Closing Thoughts
The v15 → v16 upgrade is the most involved ERPNext upgrade in years, because it’s really three upgrades in one: a Python upgrade, a Node upgrade, and the framework upgrade itself. The pattern behind nearly every failure is the same: something assumed the old environment. The env pointed at old Python, supervisor pointed at old Node, an app pointed at removed APIs.
So the formula is simple, even if the execution isn’t: verify the prerequisites ruthlessly, rehearse on staging, snapshot before you touch production, and upgrade the runtime before you switch branches. Do those four things and the upgrade is boring — which is exactly what you want an upgrade to be.
Have you hit an error not covered here? Drop it in the comments with the full traceback and I’ll add it to the list.