
Woocommerce Performance
- 64 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Fixes slow WooCommerce stores by optimizing queries, clearing transients, enabling Redis object caching, and configuring page caching.
About
Diagnoses and resolves WooCommerce performance issues from un-indexed meta queries, autoload bloat, and session-table growth using profiling, caching, and maintenance routines. A developer uses it when a store slows under traffic or MySQL is the bottleneck.
- Query profiling with Query Monitor and Redis object cache
- Autoload and wp_woocommerce_sessions cleanup
Woocommerce Performance by the numbers
- 64 all-time installs (skills.sh)
- Ranked #3,123 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill woocommerce-performanceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 64 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Fixes slow WooCommerce stores by optimizing queries, clearing transients, enabling Redis object caching, and configuring page caching.
Files
WooCommerce Performance
Overview
WooCommerce performance problems typically stem from five sources: expensive product queries with un-indexed meta tables, unbounded AJAX cart/checkout calls, missing persistent object cache, bloated wp_options autoload data, and a growing wp_woocommerce_sessions and order table without cleanup. This skill covers profiling, query optimization, Redis object caching, and scheduled maintenance routines.
When to Use This Skill
- When store pages are slow to load under moderate traffic (hundreds of concurrent users)
- When server CPU spikes during WooCommerce AJAX calls (add-to-cart, shipping calculation)
- When MySQL query time shows in New Relic or Query Monitor as the primary bottleneck
- When the
wp_optionstable autoload size exceeds 1–2 MB - When
wp_woocommerce_sessionshas millions of rows slowing down session lookups - When implementing Redis or Memcached to reduce MySQL load on a high-traffic store
Core Instructions
1. Profile first with Query Monitor
Install Query Monitor plugin to identify slow queries in the admin and frontend:
# Via WP-CLI
wp plugin install query-monitor --activateFocus on:
- Queries per page load (> 50 is a concern)
- Duplicate queries (same query run multiple times)
- Slow queries (> 100ms individual queries)
- Large result sets (queries returning thousands of rows)
For production profiling, enable MySQL slow query log:
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 0.5;
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';2. Enable Redis persistent object cache
The single highest-impact optimization for WooCommerce. Without it, every page load re-fetches the same product data from MySQL:
# Install Redis server
sudo apt install redis-server
# Install the Redis Object Cache plugin
wp plugin install redis-cache --activate
wp redis enableConfigure in wp-config.php:
define('WP_REDIS_HOST', '127.0.0.1');
define('WP_REDIS_PORT', 6379);
define('WP_REDIS_DATABASE', 0);
define('WP_REDIS_TIMEOUT', 1);
define('WP_REDIS_READ_TIMEOUT', 1);
define('WP_REDIS_MAXTTL', 86400); // 24 hours max TTL
// Selective cache groups — exclude user sessions from Redis (they change constantly)
define('WP_REDIS_IGNORED_GROUPS', ['wc_session_id', 'counts', 'plugins']);3. Optimize WooCommerce-specific database queries
<?php
// Add missing indexes for common WooCommerce product queries
// Run once via WP-CLI: wp eval-file add-indexes.php
global $wpdb;
// Index for product meta queries (stock status, visibility, price)
$wpdb->query("
ALTER TABLE {$wpdb->postmeta}
ADD INDEX wc_product_meta_lookup (meta_key(32), meta_value(64))
");
// Index for order meta queries
$wpdb->query("
ALTER TABLE {$wpdb->postmeta}
ADD INDEX wc_order_customer_lookup (meta_key(32), meta_value(100))
");Use WooCommerce's HPOS (High-Performance Order Storage) to move orders out of post meta:
// wp-config.php — enable HPOS (WooCommerce 7.1+)
// This is configured via WooCommerce Settings → Advanced → Features
// Enables dedicated order tables: wp_wc_orders, wp_wc_order_items, etc. # Check HPOS status
wp wc hpos status
# Migrate orders to HPOS
wp wc hpos migrate --batch-size=5004. Clean up transients, sessions, and log tables
<?php
// Scheduled cleanup — run daily via Action Scheduler
add_action('my_plugin_daily_cleanup', function () {
global $wpdb;
// Delete expired WooCommerce sessions (older than 48 hours)
$expiry_threshold = time() - (48 * HOUR_IN_SECONDS);
$wpdb->query($wpdb->prepare("
DELETE FROM {$wpdb->prefix}woocommerce_sessions
WHERE session_expiry < %d
LIMIT 5000
", $expiry_threshold));
// Delete expired transients
$wpdb->query("
DELETE FROM {$wpdb->options}
WHERE option_name LIKE '_transient_timeout_%'
AND option_value < UNIX_TIMESTAMP()
LIMIT 5000
");
$wpdb->query("
DELETE t FROM {$wpdb->options} t
LEFT JOIN {$wpdb->options} e
ON e.option_name = REPLACE(t.option_name, '_transient_', '_transient_timeout_')
WHERE t.option_name LIKE '_transient_%'
AND e.option_id IS NULL
LIMIT 5000
");
// Rotate WooCommerce logs older than 30 days
$wpdb->query("
DELETE FROM {$wpdb->prefix}woocommerce_log
WHERE timestamp < DATE_SUB(NOW(), INTERVAL 30 DAY)
LIMIT 10000
");
});
// Register the scheduled event
add_action('init', function () {
if (!as_next_scheduled_action('my_plugin_daily_cleanup')) {
as_schedule_recurring_action(strtotime('tomorrow midnight'), DAY_IN_SECONDS, 'my_plugin_daily_cleanup');
}
});5. Optimize the wp_options autoload table
Autoloaded options are loaded on every page request. Bloated autoload causes significant overhead:
-- Find large autoloaded options
SELECT option_name, LENGTH(option_value) as size_bytes
FROM wp_options
WHERE autoload = 'yes'
ORDER BY LENGTH(option_value) DESC
LIMIT 30; <?php
// Fix: Store plugin data as transients or post meta instead of autoloaded options
// Bad — autoloaded, loaded on every request
update_option('my_plugin_product_cache', $large_array, true);
// Good — not autoloaded
update_option('my_plugin_product_cache', $large_array, false);
// Better — use transient with expiry
set_transient('my_plugin_product_cache', $large_array, HOUR_IN_SECONDS);
// Disable autoload for existing options
global $wpdb;
$wpdb->update(
$wpdb->options,
['autoload' => 'no'],
['option_name' => 'woocommerce_attribute_taxonomies']
);Examples
Fragment caching for product loops
<?php
// Cache rendered product card HTML in Redis to avoid re-rendering
function render_product_card_cached(int $product_id): string {
$cache_key = "product_card_{$product_id}_" . get_locale();
$cached = wp_cache_get($cache_key, 'product_cards');
if ($cached !== false) {
return $cached;
}
$product = wc_get_product($product_id);
if (!$product) return '';
ob_start();
wc_get_template('content-product.php', ['product' => $product]);
$html = ob_get_clean();
// Cache for 30 minutes; invalidate on product update
wp_cache_set($cache_key, $html, 'product_cards', 30 * MINUTE_IN_SECONDS);
return $html;
}
// Purge cache when product is updated
add_action('woocommerce_update_product', function (int $product_id) {
wp_cache_delete("product_card_{$product_id}_" . get_locale(), 'product_cards');
// Also delete all locale variants
wp_cache_flush_group('product_cards');
});Detect and fix N+1 product queries
<?php
// Bad — triggers N+1 queries (one per product in loop)
$product_ids = wc_get_featured_product_ids();
foreach ($product_ids as $id) {
$product = wc_get_product($id); // <-- separate query for each product
echo $product->get_name();
}
// Good — prime the cache before the loop
$product_ids = wc_get_featured_product_ids();
// Pre-warm the object cache with all products in a single query
$products = array_map('wc_get_product', $product_ids); // Still N queries without this:
// Better — use WC_Product_Query with ID filter (single query)
$products = wc_get_products([
'include' => $product_ids,
'limit' => count($product_ids),
'return' => 'objects',
]);
foreach ($products as $product) {
echo $product->get_name(); // data already loaded
}Best Practices
- Enable HPOS (High-Performance Order Storage) on WooCommerce 7.1+ stores — it moves order data into dedicated tables with proper indexes, eliminating the
wp_postmetabottleneck for orders - Add Redis/Memcached object cache before anything else — it eliminates redundant MySQL queries that WordPress and WooCommerce make on every request and often cuts DB load by 60–80%
- Run cleanup on off-peak hours via Action Scheduler — batch DELETE operations (limit 5000 rows per run) to avoid long table locks during cleanup
- Use `WP_DEBUG_LOG` with `SAVEQUERIES` only on dev — enabling these on production kills performance; use APM tools (New Relic, Datadog) for production profiling
- Paginate admin order queries — loading all orders in one go (
posts_per_page: -1) locks MySQL and causes timeouts; always usepagedwithposts_per_page≤ 100 - Set `WP_REDIS_IGNORED_GROUPS` to exclude volatile data (cart, session counters) from Redis to prevent cache stampedes on high-traffic checkout pages
- Monitor `wp_options` table size weekly — set up an alert if autoloaded data exceeds 800KB; common culprits are plugins that store large arrays as autoloaded options
Common Pitfalls
| Problem | Solution |
|---|---|
| Redis cache not reducing MySQL queries | Check wp_cache_get hit rate in Query Monitor — a low hit rate means cache keys are being invalidated too aggressively or TTLs are too short |
| Cleanup queries cause table-level locks | Use row-level LIMIT clauses in DELETE queries (max 5000 rows per run) and schedule them during low-traffic windows |
| HPOS migration breaks third-party plugins | Audit all plugins for get_post_meta(order_id, ...) usage before enabling HPOS — these must be updated to use $order->get_meta() |
| Slow product listing despite indexes | Check whether wc_lookup_table_enabled is active; WooCommerce 3.7+ introduced wp_wc_product_meta_lookup table that dramatically speeds up product queries |
| Object cache stale after product import | Call wp_cache_flush() or wp_cache_flush_group('products') at the end of bulk import scripts to invalidate stale product caches |
| Session table grows despite cleanup | Ensure WC_Session_Handler is using the database backend (not PHP sessions); default cookie-based sessions don't create DB rows, but custom plugins may force DB sessions |
Related Skills
- @woocommerce-plugin-development
- @woocommerce-rest-api
- @database-query-optimization
- @caching-strategies
- @infrastructure-performance
{
"context": "Tests whether the agent implements the WooCommerce maintenance plugin using Action Scheduler for scheduling, applies the correct expiry thresholds and LIMIT sizes for each DELETE operation, and uses a two-step approach for transient cleanup.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Action Scheduler used",
"max_score": 12,
"description": "Plugin uses as_schedule_recurring_action() (not wp_schedule_event) to register the daily cleanup"
},
{
"name": "No double-registration guard",
"max_score": 8,
"description": "Uses as_next_scheduled_action() to check before calling as_schedule_recurring_action() (prevents duplicate schedule entries on re-activation)"
},
{
"name": "Session expiry threshold is 48 hours",
"max_score": 12,
"description": "The session DELETE query uses a threshold of 48 hours (time() - 48 * HOUR_IN_SECONDS or equivalent) not any other duration"
},
{
"name": "Session DELETE uses LIMIT 5000",
"max_score": 10,
"description": "The DELETE query for woocommerce_sessions includes LIMIT 5000 (not unlimited, not a different limit)"
},
{
"name": "Transient timeout step",
"max_score": 10,
"description": "First transient DELETE targets option_name LIKE '_transient_timeout_%' with option_value < UNIX_TIMESTAMP() and includes LIMIT 5000"
},
{
"name": "Orphaned transient step",
"max_score": 10,
"description": "Second transient DELETE removes orphaned _transient_ entries that have no matching _transient_timeout_ row (via LEFT JOIN or subquery), with LIMIT 5000"
},
{
"name": "Log rotation interval is 30 days",
"max_score": 10,
"description": "The woocommerce_log DELETE query uses an interval of 30 days (DATE_SUB(NOW(), INTERVAL 30 DAY) or equivalent)"
},
{
"name": "Log DELETE uses LIMIT 10000",
"max_score": 10,
"description": "The DELETE query for woocommerce_log includes LIMIT 10000"
},
{
"name": "Cleanup scheduled at off-peak",
"max_score": 8,
"description": "The recurring schedule is set to run at a low-traffic time (e.g., midnight / 'tomorrow midnight' or another off-peak offset) rather than immediately or at an arbitrary offset"
},
{
"name": "Batch size rationale documented",
"max_score": 10,
"description": "cleanup-design.md explains that LIMIT values are chosen to avoid long table locks or equivalent explanation for batching"
}
]
}
WooCommerce Database Maintenance Plugin
Problem Description
A busy WooCommerce store has been running for three years without any database maintenance. The operations team has flagged that the wp_woocommerce_sessions table has grown to over 4 million rows (most of them abandoned sessions from bots and expired guest checkouts), expired transients are cluttering wp_options, and the WooCommerce log table is consuming gigabytes of disk space. Nightly backups are taking too long and the DB server is showing elevated I/O during business hours.
The team wants a WordPress plugin that runs automated cleanup on a daily schedule. A previous developer started work on the file below but never finished it — your job is to complete the implementation so it correctly handles all three cleanup tasks. The plugin must be safe to run on a live store: large DELETE operations must be batched appropriately, and the schedule must be registered so it only fires once per day and won't double-register on repeated plugin activations.
Output Specification
Produce a single PHP file named wc-maintenance.php that contains a complete, working WordPress plugin implementing the daily cleanup routine. The plugin header (Plugin Name, Description, Version) should be included.
Also produce a cleanup-design.md that briefly describes:
- The scheduling mechanism used and why it was chosen
- The batch size decisions and their rationale
- The session expiry window used
Input Files
The following starter file is provided. Extract it before beginning.
=============== FILE: inputs/wc-maintenance-stub.php =============== <?php /**
- Plugin Name: WC Maintenance
- Description: Daily WooCommerce database cleanup
- Version: 0.1.0
*/
// TODO: Register the daily cleanup action using the correct scheduling API // TODO: Hook the cleanup function to the registered action
function wc_maintenance_run_cleanup() { global $wpdb;
// TODO: Delete expired WooCommerce sessions // Sessions are stored in {prefix}woocommerce_sessions // A session is expired when session_expiry timestamp is in the past
// TODO: Delete expired transients from wp_options // Step 1: delete _transient_timeout_ entries that are past due // Step 2: delete orphaned _transient_ entries with no matching timeout entry
// TODO: Delete old WooCommerce log entries // Logs are stored in {prefix}woocommerce_log }
{
"context": "Tests whether the agent fixes N+1 product queries using wc_get_products() with batch loading, implements fragment caching with the correct group name and TTL, hooks cache invalidation on product update, and fixes unbounded order queries with pagination.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Batch product fetch",
"max_score": 12,
"description": "featured-products-widget.php uses wc_get_products() with an 'include' parameter to fetch all products in a single query, rather than calling wc_get_product() in a loop"
},
{
"name": "wp_cache_get used",
"max_score": 8,
"description": "featured-products-widget.php calls wp_cache_get() before rendering to attempt a cache hit"
},
{
"name": "product_cards cache group",
"max_score": 10,
"description": "wp_cache_get() and wp_cache_set() use 'product_cards' as the cache group name"
},
{
"name": "30-minute TTL",
"max_score": 10,
"description": "wp_cache_set() uses a TTL of 30 minutes (1800 seconds or 30 * MINUTE_IN_SECONDS)"
},
{
"name": "Locale in cache key",
"max_score": 8,
"description": "The cache key includes get_locale() to avoid serving the wrong locale's cached output"
},
{
"name": "Cache invalidation hook",
"max_score": 10,
"description": "featured-products-widget.php adds an action on 'woocommerce_update_product' to invalidate cached product output"
},
{
"name": "wp_cache_flush_group used",
"max_score": 10,
"description": "The invalidation handler calls wp_cache_flush_group('product_cards') to clear all locale variants"
},
{
"name": "No limit=-1 in order query",
"max_score": 10,
"description": "admin-order-report.php does NOT use 'limit' => -1 (or posts_per_page=-1) for the order query"
},
{
"name": "Paginated order query",
"max_score": 12,
"description": "admin-order-report.php uses a 'paged' or 'page' parameter with a limit of 100 or less per page to paginate through orders"
},
{
"name": "Cache key includes product_id",
"max_score": 10,
"description": "Cache key is scoped per product_id (not a single key for the entire widget)"
}
]
}
WooCommerce Featured Products Widget with Caching
Problem Description
A home goods retailer has a custom "Featured Products" widget on their homepage that currently causes serious performance problems. Every page load executes a separate database query for each featured product, and the rendered HTML is re-generated from scratch on every request — even though featured products rarely change. On high-traffic days the widget alone accounts for over 40% of page-level database queries.
The team needs this widget rewritten to be cache-friendly. They want the product data fetched in as few queries as possible and the rendered HTML cached so repeated requests serve it from memory. When a featured product is updated in the admin, the cache for that product's rendered output must be invalidated. The current broken implementation is provided below.
Additionally, the store's admin has a report page that loops through orders to generate a revenue summary. That query is currently written to fetch all orders at once and has caused MySQL timeouts on stores with large order histories. The admin report code also needs to be corrected.
Output Specification
Produce two PHP files:
featured-products-widget.php— the corrected widget implementation with fragment cachingadmin-order-report.php— the corrected order query for the admin revenue report
Also produce optimization-notes.md briefly describing the caching strategy used for the widget (cache key scheme, group name, TTL, and invalidation approach).
Input Files
The following files are provided. Extract them before beginning.
=============== FILE: inputs/featured-products-widget-broken.php =============== <?php /**
- Broken Featured Products Widget
- Problems: N+1 queries, no caching, no cache invalidation
*/
function render_featured_products(): string { $product_ids = wc_get_featured_product_ids(); $html = '';
foreach ($product_ids as $id) { // N+1: separate DB query per product $product = wc_get_product($id); if (!$product) continue;
ob_start(); wc_get_template('content-product.php', ['product' => $product]); $html .= ob_get_clean(); }
return $html; }
=============== FILE: inputs/admin-order-report-broken.php =============== <?php /**
- Broken Admin Order Report
- Problem: fetches all orders at once — causes MySQL timeouts on large stores
*/
function get_revenue_summary(): array { // BAD: loads every order into memory at once $orders = wc_get_orders([ 'status' => ['wc-completed', 'wc-processing'], 'limit' => -1, 'return' => 'objects', ]);
$total = 0; $count = 0; foreach ($orders as $order) { $total += $order->get_total(); $count++; }
return ['total' => $total, 'count' => $count]; }
{
"context": "Tests whether the agent correctly configures the Redis object cache for WooCommerce, including all required wp-config.php constants, proper TTL settings, and exclusion of volatile cache groups that would cause cache stampedes on checkout.",
"type": "weighted_checklist",
"checklist": [
{
"name": "WP_REDIS_HOST defined",
"max_score": 6,
"description": "redis-config.php defines WP_REDIS_HOST (e.g., '127.0.0.1')"
},
{
"name": "WP_REDIS_PORT defined",
"max_score": 6,
"description": "redis-config.php defines WP_REDIS_PORT (e.g., 6379)"
},
{
"name": "WP_REDIS_DATABASE defined",
"max_score": 6,
"description": "redis-config.php defines WP_REDIS_DATABASE"
},
{
"name": "WP_REDIS_TIMEOUT defined",
"max_score": 6,
"description": "redis-config.php defines WP_REDIS_TIMEOUT"
},
{
"name": "WP_REDIS_READ_TIMEOUT defined",
"max_score": 6,
"description": "redis-config.php defines WP_REDIS_READ_TIMEOUT"
},
{
"name": "MAXTTL set to 86400",
"max_score": 12,
"description": "WP_REDIS_MAXTTL is defined and set to 86400 (24 hours)"
},
{
"name": "wc_session_id excluded",
"max_score": 14,
"description": "WP_REDIS_IGNORED_GROUPS includes 'wc_session_id'"
},
{
"name": "counts excluded",
"max_score": 10,
"description": "WP_REDIS_IGNORED_GROUPS includes 'counts'"
},
{
"name": "plugins excluded",
"max_score": 10,
"description": "WP_REDIS_IGNORED_GROUPS includes 'plugins'"
},
{
"name": "Plugin install command",
"max_score": 8,
"description": "setup-notes.md includes 'wp plugin install redis-cache' (or equivalent WP-CLI command to install the Redis Object Cache plugin)"
},
{
"name": "wp redis enable command",
"max_score": 8,
"description": "setup-notes.md includes 'wp redis enable' to activate the object cache drop-in"
},
{
"name": "Volatile data exclusion rationale",
"max_score": 8,
"description": "setup-notes.md explains that session/cart data is excluded from Redis because it changes on every request (or equivalent: to prevent cache stampedes / stale session data on checkout)"
}
]
}
WooCommerce Redis Caching Setup
Problem Description
A mid-sized fashion retailer running WooCommerce is experiencing significant database load during peak hours. Their DBA has identified that MySQL is handling thousands of identical queries per minute — most of them fetching the same product data, shipping zone configuration, and tax rules over and over on every page request. The team has provisioned a Redis server at 127.0.0.1:6379 and wants it wired into WordPress as a persistent object cache.
The store runs WooCommerce on a standard LEMP stack. They want an experienced developer to produce a production-ready wp-config.php snippet that enables Redis object caching correctly. They're particularly concerned about cache correctness on the checkout path, where volatile data (sessions, cart counters) must never be served stale, and they want a sensible maximum TTL so memory doesn't grow unbounded.
Output Specification
Produce a file named redis-config.php containing only the define() constants that should be added to wp-config.php to configure the Redis object cache. The file should include brief inline comments explaining the purpose of each constant.
Also produce a short setup-notes.md explaining: 1. Which WP-CLI commands are needed to install and activate the required plugin and enable the cache 2. Why certain cache groups are excluded from Redis in this configuration
{
"name": "finsi/woocommerce-performance",
"version": "0.1.0",
"summary": "WooCommerce optimization — query tuning, caching, and database cleanup",
"skills": {
"woocommerce-performance": {
"path": "SKILL.md"
}
}
}