1. Challenges Faced During Migration

1.1 V14 → V15 Specific Challenges

1.1.1 Frappe V15 Python & Dependency Changes

Frappe v15 raised the minimum Python version to 3.10+ (strict) and Node to 18+. While Python 3.10 itself was already used in many v14 deployments, v15 introduced stricter typing enforcement and dropped several legacy utility modules.

frappe.utils.make_esc() removed in v15 — custom apps using it for SQL escaping broke

frappe.model.meta cache behaviour changed — custom apps that stored meta references across requests saw stale data

Node 14/16 on production server had to be upgraded to Node 18 before bench build would succeed

1.1.2 V15 ORM and DB API Changes

V14 Behaviour

V15 Change

frappe.db.sql() accepting positional %s params without explicit values tuple

Now requires explicit values param always; missing values raised InterfaceError

frappe.get_all() returning extra internal fields (__islocal etc.)

Stripped from results in v15; custom code relying on them broke silently

frappe.db.set_value() auto-committing

No longer auto-commits; explicit frappe.db.commit() needed in certain contexts

DocType meta accessed via frappe.get_meta() returning cached mutable object

Returns a copy in v15; mutations to the returned object no longer affect cache

1.1.3 V15 hooks.py Structural Changes

app_include_js / app_include_css path resolution changed — assets now served from /assets/<app>/dist/ post-build

website_route_rules added new required fields; incomplete entries silently ignored in v14 now raised warnings in v15

on_login and on_logout hooks were deprecated in v15 in favour of on_session_creation and on_session_update

jinja_filters hook renamed to jinja in v15; old key silently ignored causing all custom Jinja filters to stop working

1.1.4 V15 Background Worker Changes

Frappe v15 began the transition away from the original RQ worker model, introducing a new worker supervisor structure that was fully completed in v16.

Worker count configuration moved from Procfile to a new workers config section in common_site_config.json

bench setup supervisor generated different worker service names in v15 — old supervisor configs had to be fully regenerated

frappe.enqueue() with at_front=True behaviour changed; jobs with at_front were not always placed at front of queue under high load

1.2 V15 → V16 Specific Challenges

1.2.1 Vue 2 to Vue 3 Desk Frontend

The most effort-intensive change in the entire migration. Frappe v16 completed the Vue 3 migration of the Desk UI that was started in v15.

Custom Vue 2 components using Options API syntax (data(), methods:{}) required full rewrite to Vue 3 Composition API

frappe.ui.Dialog constructor signature changed; options object keys renamed

frappe.views.List and frappe.views.Form internal event names changed — custom code hooking into these events broke

Web components using frappe.widget.* API were removed; replacements required using new Frappe Dashboard API

frappe.boot object structure reorganised in v16 — frappe.boot.sysdefaults moved to frappe.sys_defaults

1.2.2 Webpack to Vite Asset Bundler

All custom apps with webpack.config.js needed migration to vite.config.js

CommonJS require() calls inside Frappe client scripts not supported in Vite ESM mode

Bundled third-party libraries (moment.js, lodash) included via webpack ProvidePlugin no longer auto-available; must be explicitly imported

bench build --app <app> now requires proper Frappe build config in the app's package.json

1.2.3 Complete Background Job System Overhaul

V16 finalised the new background worker architecture. After the partial changes in v15, v16 removed the old RQ-based worker model entirely.

Procfile worker entries from v15 no longer valid in v16 — bench setup supervisor must be re-run

frappe.get_jobs() return structure changed to a list of job metadata dicts vs old RQ job objects

Job serialisation: any non-JSON-serialisable arguments passed to frappe.enqueue() now raise an explicit error instead of silently failing

frappe.enqueue_doc() with keyword argument queue='short' no longer valid; queue names changed

1.2.4 Python 3.11 and Dependency Upgrades

Package / Module

Change in V16

distutils (stdlib)

Fully removed in Python 3.12 / deprecated in 3.11; custom app setup.py broke

tomli

Built-in as tomllib in 3.11; conflict with external tomli package in requirements

PyMySQL

Version pinning changed; apps pinning old PyMySQL version had install conflicts

redis-py

Updated for Redis 7 protocol; some Redis command wrappers changed signatures

cryptography

Major version bump required for Python 3.11; incompatible if pinned to old version in custom app requirements.txt

1.2.5 V16 Frappe Python API Deprecation Completions

Deprecated / Removed in V16

V16 Replacement

frappe.whitelist() without methods=

Must specify methods=['GET'] or ['POST'] explicitly

frappe.db.exists() returning doc name

Returns True/False only; use frappe.db.get_value() for name retrieval

frappe.utils.dateutils module

frappe.utils.date_utils (renamed in v15, removed old path in v16)

frappe.session.data.user_type

frappe.get_user().doc.user_type

frappe.desk.reportview.get_filters_cond()

frappe.db.get_filters_cond() — module moved

frappe.get_all() with or_filters positional

Must use or_filters= keyword argument

frappe.db.sql() returning dicts by default

Always returns tuples; pass as_dict=True explicitly

frappe.utils.get_fullname(user)

frappe.get_fullname(user)

1.3 Infrastructure Challenges Across Both Hops

Two separate staging servers needed — one for v15, one for v16 — requiring infrastructure provisioning budget and time

Redis: v14 ran Redis 6; v15 supports Redis 6+; v16 requires Redis 7+. Redis upgrade had to be planned carefully to not disrupt v14 production

MariaDB strict mode enforcement increased from v14 to v15 to v16 — zero-date rows, data truncation issues surfaced progressively

Multi-site bench: all sites must move together — partial site migrations not supported within a single bench

Custom app repositories needed two compat branches (v15-compat and v16-compat) managed in parallel

bench migrate on v15 and v16 each ran for 4 to 10 hours on large databases — total testing window was 20+ hours across both hops