diff --git src/wp-admin/customize.php src/wp-admin/customize.php
index 7f9e5a9dc7..de236dbcb2 100644
--- src/wp-admin/customize.php
+++ src/wp-admin/customize.php
@@ -42,10 +42,34 @@ 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 an Ajax request 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 opening an Ajax request, then the changeset will get published
+		 * where WP_Customize_Manager will not have been instantiated with `settings_previewed=false`.
+		 */
+		$nonces = $wp_customize->get_nonces();
+		$request_args = array(
+			'nonce' => $nonces['save'],
+			'customize_changeset_uuid' => $wp_customize->changeset_uuid(),
+			'wp_customize' => 'on',
+			'customize_changeset_status' => 'publish',
+		);
+		ob_start();
+		?>
+		<?php wp_print_scripts( array( 'wp-util' ) ); ?>
+		<script>
+			wp.ajax.post( 'customize_save', <?php echo wp_json_encode( $request_args ); ?> );
+		</script>
+		<?php
+		$script = ob_get_clean();
+
 		wp_die(
 			'<h1>' . __( 'Your scheduled changes just published' ) . '</h1>' .
-			'<p><a href="' . esc_url( remove_query_arg( 'changeset_uuid' ) ) . '">' . __( 'Customize New Changes' ) . '</a></p>',
+			'<p><a href="' . esc_url( remove_query_arg( 'changeset_uuid' ) ) . '">' . __( 'Customize New Changes' ) . '</a></p>' . $script,
 			200
 		);
 	}
