DEV Community

Cover image for Why your WooCommerce AI Agent endpoint is slow (and how we fixed it in 30 lines)
Almin Zolotic
Almin Zolotic

Posted on

Why your WooCommerce AI Agent endpoint is slow (and how we fixed it in 30 lines)

Published by Almin Zolotic — Zologic / UCPReady


We spent most of yesterday debugging an 8-second ucp_list_products response on a live WooCommerce store with 40,000+ SKUs. The fix was 30 lines of PHP. Here's the full investigation — every dead end included — because anyone building on WooCommerce + UCP is going to hit this.


The setup

houseofparfum.nl runs UCPReady — our WooCommerce plugin that implements the Universal Commerce Protocol (UCP) over REST, MCP, and Embedded Checkout. It's been running in production since February, and UCP Playground benchmarks consistently showed ucp_list_products taking 8,432ms.

That's not a typo. Eight seconds. On every cold session.

For context: Shopify's median MCP tool call latency is 146ms. An AI agent making 4-5 tool calls in a session at 8 seconds each is going to hit platform timeouts. Sessions fail. Purchases don't complete.

We needed to find and fix the root cause.


Eliminating suspects

Suspect 1: The database query

First thing to check — is the WooCommerce product query slow?

wp eval '
$start = microtime(true);
$results = wc_get_products([
    "status" => "publish",
    "limit" => 10,
    "paginate" => true,
]);
echo "Query: " . round((microtime(true) - $start) * 1000) . "ms\n";
echo "Total: " . $results->total . "\n";
' --allow-root
Enter fullscreen mode Exit fullscreen mode

Result: 159ms for 38,433 products. Fast. Not the problem.

Suspect 2: Product transformation

UCPReady transforms WC_Product objects into UCP format on each request. How long does that take for 10 products?

wp eval '
$adapter = UCPReady\Adapters\ProductAdapter::get_instance();
$results = wc_get_products(["status" => "publish", "limit" => 10]);
$start = microtime(true);
foreach ($results as $product) {
    $adapter->to_ucp_list_format($product);
}
echo "Transform: " . round((microtime(true) - $start) * 1000) . "ms\n";
' --allow-root
Enter fullscreen mode Exit fullscreen mode

Result: 10ms. Not the problem.

Suspect 3: Redis / object cache

The store has Redis installed. Checking if it's actually connected to WordPress:

wp cache type --allow-root
# Redis ✓

wp eval '
$found = false;
wp_cache_get("ucp_product_list_82192", "ucpready", false, $found);
echo $found ? "CACHED" : "NOT CACHED";
' --allow-root
# NOT CACHED
Enter fullscreen mode Exit fullscreen mode

Redis is connected but product list cache keys aren't being populated. Separate issue — but not the 8-second culprit either.

Suspect 4: The taxonomy mapper

UCPReady maps WooCommerce categories to Google Product Taxonomy IDs. This involves loading a 482KB taxonomy file and building an inverted index. Was it rebuilding on every request?

wp eval '
$start = microtime(true);
$mapper = UCPReady\Taxonomy\TaxonomyMapper::get_instance();
$mapper->map_category(17, "en_US");
echo round((microtime(true) - $start) * 1000) . "ms\n";
' --allow-root
# 18ms
Enter fullscreen mode Exit fullscreen mode

18ms cold. Fast. And we fixed the caching here anyway — the built inverted index now persists to Redis with a 6-hour TTL. But still not the 8-second culprit.

The real test: HTTP vs WP-CLI

At this point we noticed something odd. Every WP-CLI test was fast. The same operations over HTTP were taking 8 seconds. WP-CLI has WordPress already loaded in memory. HTTP requests boot WordPress from scratch on every call.

time curl -s -o /dev/null -X POST \
  "https://houseofparfum.nl/wp-json/ucpready/v1/mcp" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ucp_list_products","arguments":{"limit":5}}}'

# real 0m8.101s
Enter fullscreen mode Exit fullscreen mode

8 seconds over HTTP. 169ms in WP-CLI. The gap is WordPress boot time.


Finding the culprit

The store has 17 active plugins. Something in that list was making every MCP request boot WordPress for 8 seconds. We tested methodically:

wp plugin deactivate google-listings-and-ads --allow-root

time curl -s -o /dev/null -X POST \
  "https://houseofparfum.nl/wp-json/ucpready/v1/mcp" \
  ...

# real 0m0.651s
Enter fullscreen mode Exit fullscreen mode

Google Listings and Ads was adding 7.5 seconds to every single MCP request.

Not because of the product query. Not because of anything UCP-related. The plugin registers expensive hooks during WordPress initialisation that fire on every request — including REST API requests that have nothing to do with Google Shopping feeds.

This isn't a UCPReady problem or even a UCP problem. It's a WooCommerce plugin ecosystem problem. Any plugin that does expensive work on init or similar hooks will slow down every MCP request on every WooCommerce store running it.


The fix: Partial Bootstrap Mode

The solution is a must-use plugin that fires before regular plugins load and removes the offenders from the active plugins list for MCP requests only. Everything else on the store — admin, storefront, Google Shopping feed generation — is completely untouched.

<?php
/**
 * UCPReady MCP Partial Bootstrap
 * Place in /wp-content/mu-plugins/ucpready-mcp-bootstrap.php
 */

if ( ! defined( 'ABSPATH' ) ) { exit; }

$ucpready_skip = array(
    'google-listings-and-ads/google-listings-and-ads.php',
    'query-monitor/query-monitor.php',
    'ajax-search-for-woocommerce/ajax-search-for-woocommerce.php',
);

$ucpready_uri    = $_SERVER['REQUEST_URI'] ?? '';
$ucpready_is_mcp = strpos( $ucpready_uri, '/ucpready/v1/mcp' ) !== false
    || strpos( $ucpready_uri, 'rest_route=/ucpready/v1/mcp' ) !== false;

if ( $ucpready_is_mcp ) {
    add_filter( 'option_active_plugins', function( $plugins ) use ( $ucpready_skip ) {
        if ( ! is_array( $plugins ) ) return $plugins;
        return array_values( array_diff( $plugins, $ucpready_skip ) );
    }, 1 );
}
Enter fullscreen mode Exit fullscreen mode

That's the entire fix. The option_active_plugins filter with priority 1 fires before any plugin's code runs, so the offending plugins never register their hooks at all.


Results

time curl -s -o /dev/null -X POST \
  "https://houseofparfum.nl/wp-json/ucpready/v1/mcp" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"ucp_list_products","arguments":{"limit":5}}}'

# real 0m0.272s
Enter fullscreen mode Exit fullscreen mode

Then we ran a full timing breakdown:

curl -w "DNS:%{time_namelookup} Connect:%{time_connect} SSL:%{time_appconnect} TTFB:%{time_starttransfer} Total:%{time_total}\n" ...

# DNS:0.001 Connect:0.001 SSL:0.038 TTFB:0.038 Total:0.267
Enter fullscreen mode