How a Bad Custom WP_Query Can Wreck Your Site

Got a call the other day from a new client. Their WooCommerce shop, with a pretty hefty catalog, was timing out. Not just slow, but completely falling over. The main shop page was the worst offender. The previous dev was, of course, long gone. Total mess.

You see this a lot. When a site gets slow, the first instinct is often to blame the host or throw more caching at it. And yeah, my first thought was to check the caching layers. Maybe a CDN could fix it. But that’s a band-aid. Caching helps logged-out users, but the admin area was crawling, and logged-in customers were still getting hammered. That tells me the problem is deeper. It’s almost always a bad query.

Finding the Rogue Query

After about ten minutes of digging, I found the culprit in the theme’s functions.php file. The old dev needed to hide a product category from the main shop page. A reasonable request. Their tool of choice? The pre_get_posts action hook. Powerful, but dangerous if you don’t know what you’re doing.

Here’s the kicker. This is the code they used:

function ahmed_exclude_category_from_shop( $query ) {
    // This is the WRONG way to do it.
    $query->set( 'tax_query', array(
        array(
            'taxonomy' => 'product_cat',
            'field'    => 'slug',
            'terms'    => 'uncategorized',
            'operator' => 'NOT IN',
        ),
    ) );
}
add_action( 'pre_get_posts', 'ahmed_exclude_category_from_shop' );

See the problem? This function runs on every single post query across the entire site. The main shop, sure, but also admin screens, menus, sidebars, AJAX requests—everything. It was forcing WordPress to add a complex tax query where it didn’t belong, grinding the database to a halt. A real nightmare. The entire concept is explained in detail in a great post by Carl Alexander, which you can find at https://carlalexander.ca/wordpress-adventurous-wp-query-class/.

The Right Way to Modify the Main Query

The fix is to be specific. You have to tell WordPress exactly *which* query you want to modify. You do that with conditional checks. In this case, we only want to change the main query, on the front-end, on the shop page. Anything else should be left alone.

function ahmed_exclude_category_from_shop( $query ) {
    // Check if we're on the front-end, in the main query, on the shop page.
    if ( ! is_admin() && $query->is_main_query() && is_shop() ) {
        $query->set( 'tax_query', array(
            array(
                'taxonomy' => 'product_cat',
                'field'    => 'slug',
                'terms'    => 'uncategorized',
                'operator' => 'NOT IN',
            ),
        ) );
    }
}
add_action( 'pre_get_posts', 'ahmed_exclude_category_from_shop' );

This version is safe. It’s surgical. It targets the one and only query it’s supposed to and leaves everything else alone. The difference was immediate. Page load times went from 20 seconds to under 2. The admin was snappy again. Problem solved.

So, What’s the Point?

WP_Query is the engine of WordPress, and pre_get_posts is like getting direct access to the ignition system. You have to respect it. Before you modify a query, always ask yourself:

  • Is this the main query for the page? Use is_main_query().
  • Should this run in the admin? If not, use ! is_admin().
  • Is there a more specific conditional, like is_shop() or is_category(), that I can use?

For secondary content, like a “Related Products” block, don’t even use this hook. Instantiate a new WP_Query() object instead. It keeps things clean. It’s about choosing the right tool for the job. Not the most powerful one.

Look, this stuff gets complicated fast. If you’re tired of debugging someone else’s mess and just want your site to work, drop my team a line. We’ve probably seen it before.

Leave a Reply

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