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

  1. An entry point constructs an upgrader and a skin.
  2. Interactive flows may request filesystem credentials.
  3. The package downloads unless a filter short-circuits the request.
  4. Files move into place.
  5. Hooks fire, and maintenance mode may toggle.
  6. Plugin or theme caches clear after success (§2.11).
  7. Core file replacement may still require a separate wp_upgrade() run for the schema (§2.4).

Reference

LayerResponsibility
WP_UpgraderOrchestration: download, unpack, install, hooks
Specialized subclassesCore, plugin, theme, and language pack upgraders
SkinsOutput channel: HTML, JSON, or buffered messages

Developer resources


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

  1. The caller constructs an upgrader and a skin.
  2. download_package() may trigger the upgrader_pre_download filter.
  3. install_package() runs the pre-install, source selection, clear destination, and post-install filters in sequence.
  4. On completion, upgrader_process_complete fires.
  5. Errors return WP_Error objects to the skin or caller.

Reference

ClassFileExtendsRole
WP_Upgraderclass-wp-upgrader.phpBase orchestration, locks, and maintenance helpers

Developer resources


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

  1. The entry point selects the appropriate class.
  2. The upgrader receives package metadata from transients or direct arguments.
  3. File operations run under maintenance mode and locks as applicable.
  4. Language_Pack_Upgrader::async_upgrade() hooks at priority 20 on upgrader_process_complete, so translations install after a core, plugin, or theme upgrade in the same request.
  5. WP_Automatic_Updater::run() may remove and re-add those listeners to control ordering during automatic batches.

Reference

ClassFileExtendsRole
Core_Upgraderclass-core-upgrader.phpWP_UpgraderCore file replacement
Plugin_Upgraderclass-plugin-upgrader.phpWP_UpgraderPlugin updates and installs
Theme_Upgraderclass-theme-upgrader.phpWP_UpgraderTheme updates and installs
Language_Pack_Upgraderclass-language-pack-upgrader.phpWP_UpgraderTranslation packages

Developer resources


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 command wp core update-db forces the database phase independently. After a core file update, confirm that db_version (and multisite site meta where applicable) matches expectations before treating the upgrade as complete.

Process flow

  1. Core_Upgrader installs the new core file tree.
  2. An admin page load or CLI run invokes wp_upgrade() when versions differ.
  3. wp_upgrade() runs silent upgrades and network upgrades as needed.
  4. Caches flush and the wp_upgrade action fires.
  5. If step 2 is skipped, the site runs new PHP with an old schema.

Reference

FunctionFileReturnsNotes
wp_upgrade()wp-admin/includes/upgrade.phpvoidSchema migration after core file replacement
dbDelta()wp-admin/includes/upgrade.phparrayApplies SQL deltas idempotently

Developer resources


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

  1. The upgrader sets a skin.
  2. Each step calls skin methods to report progress.
  3. Interactive flows print or return structured data.
  4. Background flows buffer messages.
  5. On failure, the skin or AJAX layer surfaces errors. Background runs email results only when the automatic updater handles the batch (Part 4).

Reference

ClassFileExtendsRole
WP_Upgrader_Skinclass-wp-upgrader-skin.phpBase: credentials and default feedback
Bulk_Upgrader_Skinclass-bulk-upgrader-skin.phpWP_Upgrader_SkinMulti-item progress
Plugin_Upgrader_Skinclass-plugin-upgrader-skin.phpWP_Upgrader_SkinSingle-plugin screen
Theme_Upgrader_Skinclass-theme-upgrader-skin.phpWP_Upgrader_SkinSingle-theme screen
Bulk_Plugin_Upgrader_Skinrespective fileBulk_Upgrader_SkinBulk plugin update interface
Bulk_Theme_Upgrader_Skinrespective fileBulk_Upgrader_SkinBulk theme update interface
Automatic_Upgrader_Skinclass-automatic-upgrader-skin.phpWP_Upgrader_SkinNo HTML; messages for email or logs
WP_Ajax_Upgrader_Skinclass-wp-ajax-upgrader-skin.phpAutomatic_Upgrader_SkinAJAX errors and messages

Developer resources


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

  1. Core loads dependency metadata from plugin headers.
  2. Unmet dependencies block install actions in the interface.
  3. Updates run in caller order unless external code reorders using dependency data.
  4. Activation may still fail if constraints are unmet.

Reference

ClassFileExtendsRole
WP_Plugin_Dependenciesclass-wp-plugin-dependencies.phpGraph and metadata for required plugins

Developer resources


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

  1. An update starts and may create a temporary backup.
  2. WordPress replaces the files.
  3. On failure or fatal detection, the restore runs from the temporary backup.
  4. Cron eventually deletes old temporary backups.
  5. See §2.10 for loopback details.

Reference

PhaseVersionBehavior
Safer directory moves6.2move_dir() for atomic-style moves
Manual rollback6.3Plugin and theme rollback from temporary backup
Automatic active-plugin rollback6.6Rollback after fatal detected through loopback

Developer resources


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

  1. A run calls create_lock().
  2. If the lock exists and has not expired, the new run aborts or waits per caller logic.
  3. On completion or fatal exit, release_lock() runs when the caller implements it.
  4. If a process dies without releasing the lock, the lock expires after the timeout.

Reference

Lock nameTypical timeoutUsed by
auto_updaterDefault (one hour)WP_Automatic_Updater::run()
core_updater900 seconds (15 minutes)Core_Upgrader

Developer resources


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

  1. An upgrade begins and enables maintenance mode.
  2. WordPress replaces files.
  3. On success or controlled failure, WordPress disables maintenance mode.
  4. Long batches keep maintenance mode active until the batch completes, per core behavior for that version.

Reference

MechanismRole
.maintenance fileShort-circuits front-end requests during upgrade

Developer resources


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

  1. The updater completes a plugin change.
  2. Core sends an HTTP request to the home URL.
  3. Core inspects the response for fatal markers.
  4. On a positive fatal signal, rollback may run.
  5. On a transport failure, behavior follows the current core implementation.

Reference

ContextRole
Site HealthDiagnoses loopback capability
WordPress 6.6+ auto-update rollbackFatal detection after active plugin update

Developer resources


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

  1. An upgrade succeeds.
  2. Cache cleaners run.
  3. The next call to get_plugins() or wp_get_themes() returns fresh data.
  4. Sidebar counts refresh on the next admin load if transients were cleared.
  5. Custom code that upgrades outside core paths should call the same cleaners if versions appear stale.

Reference

FunctionFileReturnsNotes
wp_clean_plugins_cache()wp-includes/plugin.phpvoidClears plugin data caches and the update_plugins transient
wp_clean_themes_cache()wp-includes/theme.phpvoidRescans themes; clears the update_themes transient
wp_clean_update_cache()wp-includes/update.phpvoidClears all three update transients and related caches

Developer resources


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

  1. A package URL downloads to a temporary file.
  2. Signature material from the update response or accompanying metadata passes into verify_file_signature().
  3. On success, installation proceeds.
  4. On WP_Error, the upgrade aborts for that package — except through the core soft-fail path described above.
  5. On unsupported environments, the operation fails with a defined error code rather than completing an unverified install.

Reference

FunctionRole
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


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

  1. The download lands in a temporary file (not necessarily under upgrade/).
  2. unpack_package() unpacks the archive into wp-content/upgrade/{working-dir}/.
  3. On success, files move to the final destination and WordPress may remove the package file.
  4. On failure, remnants may remain in both the temporary directory and upgrade/.
  5. Manual deletion or filesystem cleanup resolves hard failures.
  6. Free disk space and writable permissions remain prerequisites.

Reference

LocationTypical contentsRisk if orphaned
General temporary directory (get_temp_dir() or wp_tempnam())Downloaded .zip or .tmp before unpackLeftover large files; disk pressure
wp-content/upgrade/Cleared before unpack; extracted working folders during installName 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


Author

Photo de Quentin Le Duff

Quentin Le Duff

Quentin Le Duff is a WordPress developer for ten years, Opquast® Expert & ISTQB certified. He works with WordPress every day: developing open source plugins and themes, hosting and maintaining his clients’ websites, and writing about the WP ecosystem.