diff --git src/wp-includes/class-wp-customize-widgets.php src/wp-includes/class-wp-customize-widgets.php
index 055afa2..2b8228d 100644
--- src/wp-includes/class-wp-customize-widgets.php
+++ src/wp-includes/class-wp-customize-widgets.php
@@ -1088,9 +1088,7 @@ class WP_Customize_Widgets {
 	static function call_widget_update( $widget_id ) {
 		global $wp_registered_widget_updates, $wp_registered_widget_controls;
 
-		$options_transaction = new Options_Transaction();
-
-		$options_transaction->start();
+		self::start_capturing_option_updates();
 		$parsed_id   = self::parse_widget_id( $widget_id );
 		$option_name = 'widget_' . $parsed_id['id_base'];
 
@@ -1102,13 +1100,13 @@ class WP_Customize_Widgets {
 		if ( ! empty( $_POST['sanitized_widget_setting'] ) ) {
 			$sanitized_widget_setting = json_decode( self::get_post_value( 'sanitized_widget_setting' ), true );
 			if ( empty( $sanitized_widget_setting ) ) {
-				$options_transaction->rollback();
+				self::stop_capturing_option_updates();
 				return new WP_Error( 'malformed_data', 'Malformed sanitized_widget_setting' );
 			}
 
 			$instance = self::sanitize_widget_instance( $sanitized_widget_setting );
 			if ( is_null( $instance ) ) {
-				$options_transaction->rollback();
+				self::stop_capturing_option_updates();
 				return new WP_Error( 'unsanitary_data', 'Unsanitary sanitized_widget_setting' );
 			}
 
@@ -1143,15 +1141,15 @@ class WP_Customize_Widgets {
 		}
 
 		// Make sure the expected option was updated.
-		if ( 0 !== $options_transaction->count() ) {
-			if ( count( $options_transaction->options ) > 1 ) {
-				$options_transaction->rollback();
+		if ( 0 !== self::count_captured_options() ) {
+			if ( self::count_captured_options() > 1 ) {
+				self::stop_capturing_option_updates();
 				return new WP_Error( 'unexpected_update', 'Widget unexpectedly updated more than one option.' );
 			}
 
-			$updated_option_name = key( $options_transaction->options );
+			$updated_option_name = key( self::get_captured_options() );
 			if ( $updated_option_name !== $option_name ) {
-				$options_transaction->rollback();
+				self::stop_capturing_option_updates();
 				return new WP_Error( 'wrong_option', sprintf( 'Widget updated option "%1$s", but expected "%2$s".', $updated_option_name, $option_name ) );
 			}
 		}
@@ -1172,7 +1170,7 @@ class WP_Customize_Widgets {
 			$instance = $option;
 		}
 
-		$options_transaction->rollback();
+		self::stop_capturing_option_updates();
 		return compact( 'instance', 'form' );
 	}
 
@@ -1229,140 +1227,107 @@ class WP_Customize_Widgets {
 
 		wp_send_json_success( compact( 'form', 'instance' ) );
 	}
-}
 
-class Options_Transaction {
+	/***************************************************************************
+	 * Option Update Capturing
+	 ***************************************************************************/
 
 	/**
-	 * @var array $options values updated while transaction is open
+	 * @var array $_captured_options values updated while capturing is happening
 	 */
-	public $options = array();
-
-	protected $_ignore_transients = true;
-	protected $_is_current = false;
-	protected $_operations = array();
-
-	function __construct( $ignore_transients = true ) {
-		$this->_ignore_transients = $ignore_transients;
-	}
+	protected static $_captured_options = array();
 
 	/**
-	 * Determine whether or not the transaction is open
-	 * @return bool
+	 * @var bool $_is_current whether capturing is currently happening or not
 	 */
-	function is_current() {
-		return $this->_is_current;
-	}
+	protected static $_is_capturing_option_updates = false;
 
 	/**
 	 * @param $option_name
 	 * @return boolean
 	 */
-	function is_option_ignored( $option_name ) {
-		return ( $this->_ignore_transients && 0 === strpos( $option_name, '_transient_' ) );
+	protected static function is_option_capture_ignored( $option_name ) {
+		return ( 0 === strpos( $option_name, '_transient_' ) );
 	}
 
 	/**
-	 * Get the number of operations performed in the transaction
-	 * @return bool
+	 * Get options updated
+	 * @return array
 	 */
-	function count() {
-		return count( $this->_operations );
+	protected static function get_captured_options() {
+		return self::$_captured_options;
 	}
 
 	/**
-	 * Start keeping track of changes to options, and cache their new values
+	 * Get the number of options updated
+	 * @return bool
 	 */
-	function start() {
-		$this->_is_current = true;
-		add_action( 'added_option', array( $this, '_capture_added_option' ), 10, 2 );
-		add_action( 'updated_option', array( $this, '_capture_updated_option' ), 10, 3 );
-		add_action( 'delete_option', array( $this, '_capture_pre_deleted_option' ), 10, 1 );
-		add_action( 'deleted_option', array( $this, '_capture_deleted_option' ), 10, 1 );
+	protected static function count_captured_options() {
+		return count( self::$_captured_options );
 	}
 
 	/**
-	 * @action added_option
-	 * @param $option_name
-	 * @param $new_value
+	 * Start keeping track of changes to options, and cache their new values
 	 */
-	function _capture_added_option( $option_name, $new_value ) {
-		if ( $this->is_option_ignored( $option_name ) ) {
+	protected static function start_capturing_option_updates() {
+		if ( self::$_is_capturing_option_updates ) {
 			return;
 		}
-		$this->options[$option_name] = $new_value;
-		$operation = 'add';
-		$this->_operations[] = compact( 'operation', 'option_name', 'new_value' );
+
+		self::$_is_capturing_option_updates = true;
+		add_filter( 'pre_update_option', array( __CLASS__, '_capture_filter_pre_update_option' ), 10, 3 );
 	}
 
 	/**
-	 * @action updated_option
+	 * @access private
+	 * @param mixed $new_value
 	 * @param string $option_name
 	 * @param mixed $old_value
-	 * @param mixed $new_value
+	 * @return mixed
 	 */
-	function _capture_updated_option( $option_name, $old_value, $new_value ) {
-		if ( $this->is_option_ignored( $option_name ) ) {
+	static function _capture_filter_pre_update_option( $new_value, $option_name, $old_value ) {
+		if ( self::is_option_capture_ignored( $option_name ) ) {
 			return;
 		}
-		$this->options[$option_name] = $new_value;
-		$operation = 'update';
-		$this->_operations[] = compact( 'operation', 'option_name', 'old_value', 'new_value' );
-	}
-
-	protected $_pending_delete_option_autoload;
-	protected $_pending_delete_option_value;
 
-	/**
-	 * It's too bad the old_value and autoload aren't passed into the deleted_option action
-	 * @action delete_option
-	 * @param string $option_name
-	 */
-	function _capture_pre_deleted_option( $option_name ) {
-		if ( $this->is_option_ignored( $option_name ) ) {
-			return;
+		if ( ! isset( self::$_captured_options[$option_name] ) ) {
+			add_filter( "pre_option_{$option_name}", array( __CLASS__, '_capture_filter_pre_get_option' ) );
 		}
-		global $wpdb;
-		$autoload = $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $option_name ) ); // db call ok; no-cache ok
-		$this->_pending_delete_option_autoload = $autoload;
-		$this->_pending_delete_option_value    = get_option( $option_name );
+
+		self::$_captured_options[$option_name] = $new_value;
+
+		return $old_value;
 	}
 
 	/**
-	 * @action deleted_option
-	 * @param string $option_name
+	 * @access private
+	 * @param mixed $value
+	 * @return mixed
 	 */
-	function _capture_deleted_option( $option_name ) {
-		if ( $this->is_option_ignored( $option_name ) ) {
-			return;
+	static function _capture_filter_pre_get_option( $value ) {
+		$option_name = preg_replace( '/^pre_option_/', '', current_filter() );
+		if ( isset( self::$_captured_options[$option_name] ) ) {
+			$value = self::$_captured_options[$option_name];
+			$value = apply_filters( 'option_' . $option_name, $value );
 		}
-		unset( $this->options[$option_name] );
-		$operation = 'delete';
-		$old_value = $this->_pending_delete_option_value;
-		$autoload  = $this->_pending_delete_option_autoload;
-		$this->_operations[] = compact( 'operation', 'option_name', 'old_value', 'autoload' );
+
+		return $value;
 	}
 
 	/**
 	 * Undo any changes to the options since start() was called
 	 */
-	function rollback() {
-		remove_action( 'updated_option', array( $this, '_capture_updated_option' ), 10, 3 );
-		remove_action( 'added_option', array( $this, '_capture_added_option' ), 10, 2 );
-		remove_action( 'delete_option', array( $this, '_capture_pre_deleted_option' ), 10, 1 );
-		remove_action( 'deleted_option', array( $this, '_capture_deleted_option' ), 10, 1 );
-		while ( 0 !== count( $this->_operations ) ) {
-			$option_operation = array_pop( $this->_operations );
-			if ( 'add' === $option_operation['operation'] ) {
-				delete_option( $option_operation['option_name'] );
-			}
-			else if ( 'delete' === $option_operation['operation'] ) {
-				add_option( $option_operation['option_name'], $option_operation['old_value'], null, $option_operation['autoload'] );
-			}
-			else if ( 'update' === $option_operation['operation'] ) {
-				update_option( $option_operation['option_name'], $option_operation['old_value'] );
-			}
+	protected static function stop_capturing_option_updates() {
+		if ( ! self::$_is_capturing_option_updates ) {
+			return;
+		}
+
+		remove_filter( '_capture_filter_pre_update_option', array( __CLASS__, '_capture_filter_pre_update_option' ), 10, 3 );
+		foreach ( array_keys( self::$_captured_options ) as $option_name ) {
+			remove_filter( "pre_option_{$option_name}", array( __CLASS__, '_capture_filter_pre_get_option' ) );
 		}
-		$this->_is_current = false;
+
+		self::$_captured_options = array();
+		self::$_is_capturing_option_updates = false;
 	}
 }
diff --git src/wp-includes/option.php src/wp-includes/option.php
index 0091c12..4cc8c28 100644
--- src/wp-includes/option.php
+++ src/wp-includes/option.php
@@ -255,6 +255,17 @@ function update_option( $option, $value ) {
 	 */
 	$value = apply_filters( 'pre_update_option_' . $option, $value, $old_value );
 
+	/**
+	 * Filter an option before its value is (maybe) serialized and updated.
+	 *
+	 * @since 3.9.0
+	 *
+	 * @param mixed  $value     The new, unserialized option value.
+	 * @param string $option    Name of the option.
+	 * @param mixed  $old_value The old option value.
+	 */
+	$value = apply_filters( 'pre_update_option', $value, $option, $old_value );
+
 	// If the new and old values are the same, no need to update.
 	if ( $value === $old_value )
 		return false;
