diff --git src/wp-admin/customize.php src/wp-admin/customize.php
index 7f9e5a9dc7..dd67d1bc3b 100644
--- src/wp-admin/customize.php
+++ src/wp-admin/customize.php
@@ -42,7 +42,18 @@ if ( $wp_customize->changeset_post_id() ) {
 		get_post_time( 'G', true, $changeset_post ) < time()
 	);
 	if ( $missed_schedule ) {
-		wp_publish_post( $changeset_post->ID );
+		/*
+		 * Note that cron is spawned here instead of just calling `wp_publish_post( $changeset_post->ID )`.
+		 * The reason for this is that WP_Customize_Manager is not instantiated for customize.php with
+		 * the `settings_previewed=false` argument. Because of this, settings cannot by reliably saved just
+		 * since some settings will short-circuit if they detect that the current value is the same as
+		 * the value to be saved. This is particularly true for options due to logic in the update_option()
+		 * function. For more, See #39221. So by spawning cron, then the changeset will get published in
+		 * the request to `wp-cron.php` where WP_Customize_Manager will get instantiated with
+		 * `settings_previewed=false` according to `_wp_customize_publish_changeset()`.
+		 */
+		spawn_cron();
+
 		wp_die(
 			'<h1>' . __( 'Your scheduled changes just published' ) . '</h1>' .
 			'<p><a href="' . esc_url( remove_query_arg( 'changeset_uuid' ) ) . '">' . __( 'Customize New Changes' ) . '</a></p>',
