A Performant Way To Batch Delete Transient Data in WordPress

As a developer working with WordPress and WooCommerce, I recently had the chance to tackle a small but meaningful optimization. Transients in WordPress are a handy way to store temporary data. The standard approach requires looping through a list and calling `delete_transient()` for each one individually, which, while functional, isn’t as efficient as it could be. I saw an opportunity to improve this process and submitted a pull request to WooCommerce to introduce a new function: `_wc_delete_transients()`.

You can check out the full details in the pull request here: [WooCommerce PR #55931](https://github.com/woocommerce/woocommerce/pull/55931).

The idea behind `_wc_delete_transients()` is simple, rather than deleting transients one by one, it handles them in a single operation whenever possible, leveraging a direct database query. This approach cuts down on overhead and speeds things up significantly, especially when you’re working with hundreds of transients at a time. Of course, it’s designed to play nicely with WordPress’s architecture: it falls back to individual deletions if an external object cache is in use and includes safeguards like chunking large batches to avoid hitting database limits.

To see how it performs, I ran some tests. Deleting 500 transients using the traditional loop with `delete_transient() and compare the results with `_wc_delete_transients()`. The results showed that `_wc_delete_transients()` is over 100% more performant. Put another way, over twice as fast. While these numbers come from a controlled test, they suggest a solid performance boost for WooCommerce stores that need to clear out transients in bulk, like during cache purges or updates.

I kept the implementation cautious and practical. It respects WordPress’s caching mechanisms, updates the `alloptions` cache when needed, and includes error handling to log any issues that might pop up. It’s marked as an internal function, meaning it’s intended for WooCommerce’s core use rather than direct public access, but I hope it proves useful behind the scenes.

This was a modest project, but it’s satisfying to see how a targeted tweak can make a difference. For WooCommerce developers and store owners, faster transient deletion could mean quicker maintenance tasks and a slightly snappier experience overall.

Here is the full implementation
/**
 * Delete multiple transients in a single operation.
 *
 * IMPORTANT: This is a private function (internal use ONLY).
 *
 * This function efficiently deletes multiple transients at once, using a direct
 * database query when possible for better performance.
 *
 * @internal
 *
 * @since 9.8.0
 * @param array $transients Array of transient names to delete (without the '_transient_' prefix).
 * @return bool True on success, false on failure.
 */
function _wc_delete_transients( $transients ) {
	global $wpdb;

	if ( empty( $transients ) || ! is_array( $transients ) ) {
		return false;
	}

	// If using external object cache, delete each transient individually.
	if ( wp_using_ext_object_cache() ) {
		foreach ( $transients as $transient ) {
			delete_transient( $transient );
		}
		return true;
	} else {
		// For database storage, create a list of transient option names.
		$transient_names = array();
		foreach ( $transients as $transient ) {
			$transient_names[] = '_transient_' . $transient;
			$transient_names[] = '_transient_timeout_' . $transient;
		}

		// Limit the number of items in a single query to avoid exceeding database query parameter limits.
		if ( count( $transients ) > 199 ) {
			// Process in smaller chunks to reduce memory usage.
			$chunks  = array_chunk( $transients, 100 );
			$success = true;

			foreach ( $chunks as $chunk ) {
				$result = _wc_delete_transients( $chunk );
				if ( ! $result ) {
					$success = false;
				}
				// Force garbage collection after each chunk to free memory.
				gc_collect_cycles();
			}

			return $success;
		}

		try {
			// Before deleting, get the list of options to clear from cache.
			// Since we already have the option names we could skip this step but this mirrors WP's delete_option functionality.
			// It also allows us to only delete the options we know exist.
			$options_to_clear = array();
			if ( ! wp_installing() ) {
				$options_to_clear = $wpdb->get_col(
					$wpdb->prepare(
						'SELECT option_name FROM ' . $wpdb->options . ' WHERE option_name IN ( ' . implode( ', ', array_fill( 0, count( $transient_names ), '%s' ) ) . ' )',
						$transient_names
					)
				);
			}

			if ( empty( $options_to_clear ) ) {
				// If there are no options to clear, return true immediately.
				return true;
			}

			// Use a single query for better performance.
			$wpdb->query(
				$wpdb->prepare(
					'DELETE FROM ' . $wpdb->options . ' WHERE option_name IN ( ' . implode( ', ', array_fill( 0, count( $options_to_clear ), '%s' ) ) . ' )',
					$options_to_clear
				)
			);

			// Lets clear our options data from the cache.
			// We can batch delete if available, introduced in WP 6.0.0.
			if ( ! wp_installing() ) {
				if ( function_exists( 'wp_cache_delete_multiple' ) ) {
					wp_cache_delete_multiple( $options_to_clear, 'options' );
				} else {
					foreach ( $options_to_clear as $option_name ) {
						wp_cache_delete( $option_name, 'options' );
					}
				}

				// Also update alloptions cache if needed.
				// This is required to prevent phantom transients from being returned.
				$alloptions         = wp_load_alloptions( true );
				$updated_alloptions = false;

				if ( is_array( $alloptions ) ) {
					foreach ( $options_to_clear as $option_name ) {
						if ( isset( $alloptions[ $option_name ] ) ) {
							unset( $alloptions[ $option_name ] );
							$updated_alloptions = true;
						}
					}

					if ( $updated_alloptions ) {
						wp_cache_set( 'alloptions', $alloptions, 'options' );
					}
				}
			}

			return true;
		} catch ( Exception $e ) {
			wc_get_logger()->error(
				sprintf( 'Exception when deleting transients: %s', $e->getMessage() ),
				array( 'source' => '_wc_delete_transients' )
			);
			return false;
		}
	}
}
PHP

2 responses to “A Performant Way To Batch Delete Transient Data in WordPress”

  1. tremendous! 102 2025 How Engineers Slow Their Team Down, Rubber Ducking and a Hidden CSS Performance Gem – Surf Report, Issue #6 divine

Leave a Reply

Your email address will not be published. Required fields are marked *