2.1 The application model
Application is the phase where WordPress downloads packages, unpacks them, replaces files, and completes follow-up work. When you debug failed updates, you need this model as distinct from discovery and from the database upgrade phase.
How it works
WP_Upgrader coordinates downloading, filesystem access, unpack locations under wp-content/upgrade/, install_package(), and the upgrader_process_complete action. Interactive interfaces use skins. Background updates use Automatic_Upgrader_Skin and WP_Automatic_Updater. Bulk operations track progress for skins through update_count and update_current. Replacing files is not the same as migrating the database; see §2.4. Package signature verification and staging directory behavior are covered in §2.12 and §2.13.
Process flow
- An entry point constructs an upgrader and a skin.
- Interactive flows may request filesystem credentials.
- The package downloads unless a filter short-circuits the request.
- Files move into place.
- Hooks fire, and maintenance mode may toggle.
- Plugin or theme caches clear after success (§2.11).
- Core file replacement may still require a separate
wp_upgrade()run for the schema (§2.4).
Reference
| Layer | Responsibility |
|---|---|
WP_Upgrader | Orchestration: download, unpack, install, hooks |
| Specialized subclasses | Core, plugin, theme, and language pack upgraders |
| Skins | Output channel: HTML, JSON, or buffered messages |
Developer resources
WP_Upgraderclass reference — base class for all upgraders.
2.2 WP_Upgrader: the base class
WP_Upgrader provides the shared implementation for downloading, filesystem work, unpacking, and package installation. Most customization hooks attach here or in its subclasses.
How it works
The class in wp-admin/includes/class-wp-upgrader.php coordinates downloading packages to a temporary file through download_url(), working with WP_Filesystem, unpacking ZIP files into wp-content/upgrade/ through unpack_package(), calling install_package(), and firing the upgrader_process_complete action. Bulk operations increment counters for progress skins.
Process flow
- The caller constructs an upgrader and a skin.
download_package()may trigger theupgrader_pre_downloadfilter.install_package()runs the pre-install, source selection, clear destination, and post-install filters in sequence.- On completion,
upgrader_process_completefires. - Errors return
WP_Errorobjects to the skin or caller.
Reference
| Class | File | Extends | Role |
|---|---|---|---|
WP_Upgrader | class-wp-upgrader.php | — | Base orchestration, locks, and maintenance helpers |
Developer resources
WP_Upgrader::install_package()reference — installation pipeline hooks.
2.3 Specialized upgraders
Each major package type has an upgrader class with behaviors suited to that type. When you extend update flows, you typically subclass or call these classes.
How it works
Core_Upgrader replaces WordPress core files and supports partial builds and rollback-related options in its upgrade() method. Plugin_Upgrader handles single-plugin paths, bulk_upgrade(), and ZIP or URL installs. Theme_Upgrader handles theme updates and installations. Language_Pack_Upgrader installs translation updates collected from transients.
Core file replacement through Core_Upgrader remains separate from the database upgrade routine in §2.4.
Process flow
- The entry point selects the appropriate class.
- The upgrader receives package metadata from transients or direct arguments.
- File operations run under maintenance mode and locks as applicable.
Language_Pack_Upgrader::async_upgrade()hooks at priority 20 onupgrader_process_complete, so translations install after a core, plugin, or theme upgrade in the same request.WP_Automatic_Updater::run()may remove and re-add those listeners to control ordering during automatic batches.
Reference
| Class | File | Extends | Role |
|---|---|---|---|
Core_Upgrader | class-core-upgrader.php | WP_Upgrader | Core file replacement |
Plugin_Upgrader | class-plugin-upgrader.php | WP_Upgrader | Plugin updates and installs |
Theme_Upgrader | class-theme-upgrader.php | WP_Upgrader | Theme updates and installs |
Language_Pack_Upgrader | class-language-pack-upgrader.php | WP_Upgrader | Translation packages |
Developer resources
Core_Upgraderclass reference — core updates.
2.4 Core database upgrade phase (wp_upgrade())
Replacing core files and migrating the database are separate steps. Many problems arise from conflating them.
How it works
After Core_Upgrader replaces files, the database upgrade phase runs separately through wp_upgrade() in wp-admin/includes/upgrade.php, outside WP_Upgrader::install_package(). That routine compares the stored db_version option to the global $wp_db_version. If the values differ, the routine runs pre_schema_upgrade(), make_db_current_silent(), and upgrade_all(). On multisite, upgrade_network() runs on the main site when applicable. After flushing caches, WordPress fires do_action( 'wp_upgrade', $wp_db_version, $wp_current_db_version ) where the first argument is the new database version and the second is the old. Table and schema changes typically rely on dbDelta() for idempotent SQL.
Note: Files may match a new release while
wp_upgrade()never completed or errored mid-run. The WP-CLI commandwp core update-dbforces the database phase independently. After a core file update, confirm thatdb_version(and multisite site meta where applicable) matches expectations before treating the upgrade as complete.
Process flow
Core_Upgraderinstalls the new core file tree.- An admin page load or CLI run invokes
wp_upgrade()when versions differ. wp_upgrade()runs silent upgrades and network upgrades as needed.- Caches flush and the
wp_upgradeaction fires. - If step 2 is skipped, the site runs new PHP with an old schema.
Reference
| Function | File | Returns | Notes |
|---|---|---|---|
wp_upgrade() | wp-admin/includes/upgrade.php | void | Schema migration after core file replacement |
dbDelta() | wp-admin/includes/upgrade.php | array | Applies SQL deltas idempotently |
Developer resources
wp_upgrade()reference — database upgrade routine.wp_upgradeaction reference — fires after a database upgrade.wp core update-dbCLI command — CLI database upgrade.
2.5 Upgrader skins: the output layer
Skins adapt upgrader output to full-page HTML, AJAX JSON, or silent background runs. They are not decorative themes — they are the channel between the upgrader and the runtime.
How it works
Skins subclass WP_Upgrader_Skin and implement the contract between an upgrader instance and its environment. Skins do not extend WP_Upgrader itself. Interactive admin uses skins that render progress, errors, and FTP credential forms. AJAX skins collect messages and errors for JSON. Background update skins suppress visible output and buffer credential prompts; their messages feed logs and email bodies.
The upgrader calls feedback(), error(), header(), footer(), and request_filesystem_credentials(). Strings often come from $upgrader->strings, set by subclass upgrade_strings() methods. Automatic_Upgrader_Skin buffers credential output and accumulates messages for notifications. WP_Ajax_Upgrader_Skin extends it for error collection on async responses.
Process flow
- The upgrader sets a skin.
- Each step calls skin methods to report progress.
- Interactive flows print or return structured data.
- Background flows buffer messages.
- On failure, the skin or AJAX layer surfaces errors. Background runs email results only when the automatic updater handles the batch (Part 4).
Reference
| Class | File | Extends | Role |
|---|---|---|---|
WP_Upgrader_Skin | class-wp-upgrader-skin.php | — | Base: credentials and default feedback |
Bulk_Upgrader_Skin | class-bulk-upgrader-skin.php | WP_Upgrader_Skin | Multi-item progress |
Plugin_Upgrader_Skin | class-plugin-upgrader-skin.php | WP_Upgrader_Skin | Single-plugin screen |
Theme_Upgrader_Skin | class-theme-upgrader-skin.php | WP_Upgrader_Skin | Single-theme screen |
Bulk_Plugin_Upgrader_Skin | respective file | Bulk_Upgrader_Skin | Bulk plugin update interface |
Bulk_Theme_Upgrader_Skin | respective file | Bulk_Upgrader_Skin | Bulk theme update interface |
Automatic_Upgrader_Skin | class-automatic-upgrader-skin.php | WP_Upgrader_Skin | No HTML; messages for email or logs |
WP_Ajax_Upgrader_Skin | class-wp-ajax-upgrader-skin.php | Automatic_Upgrader_Skin | AJAX errors and messages |
Developer resources
WP_Upgrader_Skinclass reference — base skin class.
2.6 WP_Plugin_Dependencies: dependency resolution (WordPress 6.5+)
Plugin dependencies declared in headers are resolved by WP_Plugin_Dependencies. Core bulk update order does not topologically sort by dependency; this API helps your custom code order work correctly.
How it works
The Requires Plugins header lists comma-separated WordPress.org plugin slugs. WP_Plugin_Dependencies in wp-includes/class-wp-plugin-dependencies.php reads headers, fetches dependency metadata, and can detect circular graphs. The install interface disables the Install Now button until requirements are met. WP-CLI enforcement is strongest on activation; multiple commands may be needed.
Core bulk updates do not reorder by dependency graph. Public helpers such as get_dependency_names() and get_dependency_filepath() help you infer dependency-before-dependent ordering, though some internals are protected.
Process flow
- Core loads dependency metadata from plugin headers.
- Unmet dependencies block install actions in the interface.
- Updates run in caller order unless external code reorders using dependency data.
- Activation may still fail if constraints are unmet.
Reference
| Class | File | Extends | Role |
|---|---|---|---|
WP_Plugin_Dependencies | class-wp-plugin-dependencies.php | — | Graph and metadata for required plugins |
Developer resources
WP_Plugin_Dependenciesclass reference — class reference.- Plugin dependencies merge announcement — feature background.
2.7 Rollback system (WordPress 6.2–6.6)
Rollback support evolved across releases: safer directory moves, manual plugin and theme rollback with temporary backups, then automatic plugin rollback after fatal errors detected through loopback.
How it works
WordPress 6.2 introduced move_dir() to centralize atomic-style moves across WP_Filesystem backends.
WordPress 6.3 added manual plugin and theme rollback. Before replacement, previous directory trees move to wp-content/upgrade-temp-backup/plugins/{slug} or .../themes/{slug}. On WP_Error from install_package() or related failures, the backup restores. Reactivation failure can also restore the prior copy. Site Health gained backup-folder-writable and disk-space tests.
WordPress 6.6 added automatic rollback for active plugins. After an update, core may send a loopback request to the home URL to detect PHP fatals and call restore_temp_backup(). Inactive plugins skip fatal detection. Email may follow a rollback.
Temporary backups are not a general user-facing rollback product. On success or after restore, WordPress clears their contents. WP_Upgrader::init() schedules weekly wp_delete_temp_updater_backups; deletion can defer when locks or AJAX requests are active.
Process flow
- An update starts and may create a temporary backup.
- WordPress replaces the files.
- On failure or fatal detection, the restore runs from the temporary backup.
- Cron eventually deletes old temporary backups.
- See §2.10 for loopback details.
Reference
| Phase | Version | Behavior |
|---|---|---|
| Safer directory moves | 6.2 | move_dir() for atomic-style moves |
| Manual rollback | 6.3 | Plugin and theme rollback from temporary backup |
| Automatic active-plugin rollback | 6.6 | Rollback after fatal detected through loopback |
Developer resources
- New in 6.3: rollback for failed manual plugin and theme updates — manual rollback feature.
- Merge proposal: rollback after auto-update — automatic rollback path.
2.8 Upgrader locks
Locks prevent concurrent upgrade processes from corrupting state. The automatic updater and the core upgrader use different lock names and timeouts.
How it works
WP_Upgrader::create_lock( $lock_name, $release_timeout ) stores a lock option with a Unix timestamp. The default release timeout when omitted is one hour. WP_Automatic_Updater::run() uses the auto_updater lock with the default timeout. Core_Upgrader uses core_updater with a 15-minute timeout. release_lock() deletes the option. Stale locks block new runs until expiry or manual deletion of the lock option through appropriate APIs.
Process flow
- A run calls
create_lock(). - If the lock exists and has not expired, the new run aborts or waits per caller logic.
- On completion or fatal exit,
release_lock()runs when the caller implements it. - If a process dies without releasing the lock, the lock expires after the timeout.
Reference
| Lock name | Typical timeout | Used by |
|---|---|---|
auto_updater | Default (one hour) | WP_Automatic_Updater::run() |
core_updater | 900 seconds (15 minutes) | Core_Upgrader |
Developer resources
WP_Upgrader::create_lock()reference — lock acquisition.WP_Upgrader::release_lock()reference — lock release.
2.9 Maintenance mode
Maintenance mode limits front-end execution while files are inconsistent. Automatic batches may hold maintenance mode longer to avoid serving half-updated code.
How it works
WP_Upgrader::maintenance_mode( true | false ) toggles the .maintenance file during updates. For automatic updates, maintenance may remain enabled for the entire batch on supported versions so that front-end requests do not hit partially updated code during rollback-related work.
Process flow
- An upgrade begins and enables maintenance mode.
- WordPress replaces files.
- On success or controlled failure, WordPress disables maintenance mode.
- Long batches keep maintenance mode active until the batch completes, per core behavior for that version.
Reference
| Mechanism | Role |
|---|---|
.maintenance file | Short-circuits front-end requests during upgrade |
Developer resources
- WordPress upgrade process overview — describes the
.maintenancefile behavior.
2.10 Loopback requests in the update pipeline
Loopback requests are HTTP requests from PHP to the same site’s front end. They matter for Site Health and for automatic plugin rollback after fatal errors.
How it works
A loopback typically targets home_url( '/' ) with scrape query arguments. Site Health’s loopback test checks whether self-requests succeed.
WordPress 6.6+ automatic updater fatal detection uses wp_remote_get() on the home URL. If the response is a transport WP_Error, core may treat that like a fatal for rollback purposes, so infrastructure failures can trigger a restore. Validate the behavior in WP_Automatic_Updater::has_fatal_error() for your version.
Localhost environments, reverse proxies, or blocked outbound HTTP can break loopbacks.
Process flow
- The updater completes a plugin change.
- Core sends an HTTP request to the home URL.
- Core inspects the response for fatal markers.
- On a positive fatal signal, rollback may run.
- On a transport failure, behavior follows the current core implementation.
Reference
| Context | Role |
|---|---|
| Site Health | Diagnoses loopback capability |
| WordPress 6.6+ auto-update rollback | Fatal detection after active plugin update |
Developer resources
wp_remote_get()reference — HTTP GET primitive used in loopback checks.
2.11 Cache invalidation after updates
After file changes, WordPress must refresh plugin and theme runtime caches so the interface and APIs reflect new versions without unnecessary HTTP requests to WordPress.org.
How it works
After successful plugin or theme updates, core calls wp_clean_plugins_cache() and wp_clean_themes_cache(). Each function clears the relevant site transient by default and refreshes object caches or rescans theme directories.
wp_clean_update_cache() deletes the update_core, update_plugins, and update_themes transients in one call and invokes the plugin and theme cleaners when available. It does not by itself contact WordPress.org.
wp_update_plugins() and wp_update_themes() perform immediate POST requests subject to internal throttles. Use them when you need fresh offer payloads in the same request, not only cleared caches.
Process flow
- An upgrade succeeds.
- Cache cleaners run.
- The next call to
get_plugins()orwp_get_themes()returns fresh data. - Sidebar counts refresh on the next admin load if transients were cleared.
- Custom code that upgrades outside core paths should call the same cleaners if versions appear stale.
Reference
| Function | File | Returns | Notes |
|---|---|---|---|
wp_clean_plugins_cache() | wp-includes/plugin.php | void | Clears plugin data caches and the update_plugins transient |
wp_clean_themes_cache() | wp-includes/theme.php | void | Rescans themes; clears the update_themes transient |
wp_clean_update_cache() | wp-includes/update.php | void | Clears all three update transients and related caches |
Developer resources
wp_clean_update_cache()reference — single-call invalidation.
2.12 Cryptographic signature verification (Ed25519)
From WordPress 5.2 onward, core can verify Ed25519 signatures on downloaded packages before trusting them for installation. This mitigates supply-chain tampering between WordPress.org and the filesystem by checking each package against keys distributed with WordPress.
How it works
verify_file_signature() in wp-admin/includes/file.php hashes the downloaded file with SHA-384 and verifies detached signatures using sodium_crypto_sign_verify_detached() against public keys from wp_trusted_keys(). Signatures are base64-encoded in transport; keys are base64-encoded public keys.
The native PHP sodium extension is used when available. Otherwise, WordPress may use the bundled sodium_compat polyfill subject to a runtime performance check. If verification cannot run in reasonable time, core returns a WP_Error with code signature_verification_unsupported rather than silently skipping crypto. If no signature accompanies the package, verification fails with signature_verification_no_signature. If no candidate signature matches any trusted key, verification fails with signature_verification_failed.
The trusted key list is filterable through wp_trusted_keys. HTTP download helpers such as download_url() integrate this path so verification occurs after the ZIP downloads and before the upgrader treats the package as trusted for extraction.
Core soft-fail behavior: For core updates only, Core_Upgrader::upgrade() can treat a signature WP_Error that carries softfail-filename in its error data as a non-fatal case. The failure may appear as feedback while the download path from the error data is still used, allowing the upgrade to continue. Other package types and errors without that escape hatch typically abort when verification returns WP_Error.
Process flow
- A package URL downloads to a temporary file.
- Signature material from the update response or accompanying metadata passes into
verify_file_signature(). - On success, installation proceeds.
- On
WP_Error, the upgrade aborts for that package — except through the core soft-fail path described above. - On unsupported environments, the operation fails with a defined error code rather than completing an unverified install.
Reference
| Function | Role |
|---|---|
verify_file_signature() | Ed25519 verification over SHA-384 hash of file on disk |
wp_trusted_keys() | Returns trusted base64-encoded public keys (filterable) |
sodium_crypto_sign_verify_detached() | Primitive used when available; sodium_compat may substitute if fast enough |
Developer resources
verify_file_signature()reference — signature verification API.wp_trusted_keys()reference — trusted signing keys.- Security in WordPress 5.2 — release notes on signatures and trust.
2.13 Staging directory lifecycle and garbage collection
Downloaded archives and extracted working trees use two different locations. download_url() writes the ZIP to a general temporary directory through wp_tempnam() and get_temp_dir(). WP_Upgrader::unpack_package() stages unpacked files under wp-content/upgrade/. That directory is the primary on-disk workspace before files move into wp-content/plugins/, wp-content/themes/, or the core root. Contents are transient; failed or interrupted runs can leave debris that interferes with later upgrades.
How it works
download_url() stores the incoming file in the temporary directory (system temp, upload temp, WP_CONTENT_DIR, or /tmp/ per get_temp_dir(), unless WP_TEMP_DIR overrides). WP_Upgrader::download_package() passes remote URLs to download_url(); the returned path then feeds unpack_package().
unpack_package() clears wp-content/upgrade/ through WP_Filesystem, builds a working subdirectory name from the package basename, and runs unzip_file() into that folder. After extraction, WordPress usually deletes the ZIP when $delete_package is true.
A stuck or partial extraction leaves directories or partial files under upgrade/. Disk-full conditions, permission errors, or killed PHP workers can orphan temporary ZIP files or upgrade/ trees. On the next run, unpack_package() attempts to clear upgrade/ before unpacking, but collisions or leftover paths can still yield WP_Error from the filesystem layer.
Core does not guarantee immediate cleanup of every partial path after every failure. Administrators may need to remove stale upgrade/ contents when troubleshooting. Rollback-related temporary trees under wp-content/upgrade-temp-backup/ follow their own deletion and cron cleanup rules (see §2.7).
Process flow
- The download lands in a temporary file (not necessarily under
upgrade/). unpack_package()unpacks the archive intowp-content/upgrade/{working-dir}/.- On success, files move to the final destination and WordPress may remove the package file.
- On failure, remnants may remain in both the temporary directory and
upgrade/. - Manual deletion or filesystem cleanup resolves hard failures.
- Free disk space and writable permissions remain prerequisites.
Reference
| Location | Typical contents | Risk if orphaned |
|---|---|---|
General temporary directory (get_temp_dir() or wp_tempnam()) | Downloaded .zip or .tmp before unpack | Leftover large files; disk pressure |
wp-content/upgrade/ | Cleared before unpack; extracted working folders during install | Name collisions; permission or disk errors on next run |
wp-content/upgrade-temp-backup/ | Rollback snapshots (see §2.7) | Space pressure; separate cleanup hooks |
Developer resources
download_url()reference — downloads throughwp_tempnam(); path comes fromget_temp_dir().get_temp_dir()reference — writable temporary directory for downloads.WP_Upgraderclass reference —unpack_package()useswp-content/upgrade/for extraction.

