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