External API calls, complex calculations, and aggregation queries should use set_transient/get_transient to avoid repeating expensive work on every page load.
Why This Matters
reduces page load time by 200-2000ms for pages making external API calls or complex queries
Impact: HIGH (reduces page load time by 200-2000ms for pages making external API calls or complex queries)
WordPress transients are key-value pairs with an expiration time, stored in wp_options (or in a persistent object cache like Redis if configured). Any operation that is slow and returns the same result for a period of time should be cached: external API calls, complex database aggregations, third-party SDK queries, and computationally expensive transformations.
Incorrect (uncached expensive operations on every request):
// ❌ HTTP request on every page load — 200-2000ms latency each timefunction get_exchange_rates() { $response = wp_remote_get( 'https://api.exchangerate.host/latest' ); if ( is_wp_error( $response ) ) { return []; } return json_decode( wp_remote_retrieve_body( $response ), true );}// ❌ Complex aggregation query on every requestfunction get_sales_stats() { global $wpdb; return $wpdb->get_results( "SELECT DATE(post_date) as date, COUNT(*) as orders, SUM(meta_value) as revenue FROM {$wpdb->posts} p JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_order_total' WHERE p.post_type = 'shop_order' AND p.post_status = 'wc-completed' GROUP BY DATE(post_date) ORDER BY date DESC LIMIT 30" ); // Runs a JOIN + GROUP BY on every page load}
Correct (transient caching with proper patterns):
// ✅ Cache external API callsfunction get_exchange_rates() { $cache_key = 'my_plugin_exchange_rates'; $cached = get_transient( $cache_key ); if ( false !== $cached ) { return $cached; // Cache hit } $response = wp_remote_get( 'https://api.exchangerate.host/latest', [ 'timeout' => 10, ]); if ( is_wp_error( $response ) ) { // On failure, try to use expired cache (stale-while-revalidate pattern) $stale = get_option( '_transient_stale_' . $cache_key ); return $stale ?: []; } $data = json_decode( wp_remote_retrieve_body( $response ), true ); // Cache for 1 hour; store a stale copy as backup set_transient( $cache_key, $data, HOUR_IN_SECONDS ); update_option( '_transient_stale_' . $cache_key, $data, 'no' ); // autoload=no return $data;}
// ✅ Cache complex queries with cache invalidation on data changefunction get_sales_stats() { $cache_key = 'my_plugin_sales_stats'; $cached = get_transient( $cache_key ); if ( false !== $cached ) { return $cached; } global $wpdb; $results = $wpdb->get_results( $wpdb->prepare( "SELECT DATE(post_date) as date, COUNT(*) as orders, SUM(meta_value) as revenue FROM {$wpdb->posts} p JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = %s WHERE p.post_type = %s AND p.post_status = %s GROUP BY DATE(post_date) ORDER BY date DESC LIMIT 30", '_order_total', 'shop_order', 'wc-completed' ) ); set_transient( $cache_key, $results, 15 * MINUTE_IN_SECONDS ); return $results;}// Invalidate when new orders are placedadd_action( 'woocommerce_order_status_completed', function() { delete_transient( 'my_plugin_sales_stats' );});
WordPress time constants:
Constant
Seconds
MINUTE_IN_SECONDS
60
HOUR_IN_SECONDS
3,600
DAY_IN_SECONDS
86,400
WEEK_IN_SECONDS
604,800
When NOT to use transients:
Data that changes on every request (user-specific session data)
Small/fast operations (a single indexed query is faster than transient overhead)
Sensitive data (transients are stored in wp_options, accessible on shared object caches)