How to Block Free Shipping for Specific WooCommerce Categories

Summary

A common WooCommerce requirement—blocking free shipping when specific category products are in the cart—fails despite using popular plugins like PW WooCommerce Exclude Free Shipping. This happens because free shipping logic executes before cart totals are finalized, and many plugins do not hook correctly into the woocommerce_package_rates filter where shipping method availability is determined dynamically.

Root Cause

Free shipping eligibility is typically based on a minimum order amount (e.g., €70) and evaluated during shipping rate calculation. The core issue:

  • Table Rate Shipping calculates free shipping when subtotal >= €70, ignoring product categories unless explicitly configured.
  • Plugins like PW WooCommerce Exclude Free Shipping only check if any product in the cart has the “exclude free shipping” option manually enabled—not based on category-based rules.
  • The plugin’s logic runs on the wrong hook (woocommerce_available_shipping_methods) or fails to return an empty array for affected packages, so the free shipping method remains visible.

Why This Happens in Real Systems

In production, shipping rules often rely on static configurations that do not support contextual rules (e.g., “free shipping only if no product from category X exists”). Real-world complexity arises because:

  • WooCommerce’s shipping engine evaluates packages per shipping zone, and categories aren’t natively integrated into free shipping rule conditions.
  • Third-party plugins often assume flat, single-condition logic (e.g., “exclude if cart total < $X” or “product meta excludes”), not multi-condition logic (e.g., “free shipping enabled unless category X present”).
  • Table Rate Shipping alone cannot conditionally “turn off” free shipping based on product attributes without custom logic.

Real-World Impact

  • Revenue leakage: Customers place orders just over €70 with excluded-category items and expect free shipping, causing dissatisfaction when charged.
  • Operational friction: Customer service tickets increase due to misunderstood shipping expectations.
  • Manual workarounds: Staff manually adjust orders or pay shipping costs, eroding margins.

Example or Code (if necessary and relevant)

add_filter('woocommerce_package_rates', 'block_free_shipping_for_category', 10, 2);
function block_free_shipping_for_category($rates, $package) {
    $excluded_category_slugs = ['dangerous-goods', 'fragile-items'];
    $has_excluded_category = false;

    foreach ($package['contents'] as $item) {
        if ($item['data'] && $item['data']->get_category_ids()) {
            if (array_intersect($excluded_category_slugs, wp_get_post_terms($item['data']->get_id(), 'product_cat', ['fields' => 'slugs']))) {
                $has_excluded_category = true;
                break;
            }
        }
    }

    if ($has_excluded_category) {
        unset($rates['table_rate_shipping:best_option']);
        // Or more safely: remove all free shipping methods
        foreach ($rates as $rate_id => $rate) {
            if ('free_shipping' === $rate->method_id) {
                unset($rates[$rate_id]);
            }
        }
    }

    return $rates;
}

How Senior Engineers Fix It

  • Hook precisely into woocommerce_package_rates, the canonical filter where shipping methods are finalized per package.
  • Avoid product meta-based plugins that don’t support category logic; instead, embed conditional rules directly.
  • Use wp_get_post_terms() to check category slugs programmatically, not relying on deprecated flags.
  • Scope the exclusion to only the relevant free shipping methods, preserving table rates and other options.
  • Test with caching, multiple shipping zones, and logged-in vs. guest sessions—free shipping rules behave differently under wc()->cart->calculate_totals().

Why Juniors Miss It

  • They confuse free shipping eligibility (based on cart total) with free shipping availability (based on runtime logic and filters).
  • They assume plugins like PW WooCommerce Exclude Free Shipping support category rules—without reading the docs or inspecting the hook.
  • They test only with one product type, missing edge cases (e.g., mixed-category carts or tax-inclusive totals).
  • They bypass WC’s shipping package architecture and try manipulating $cart->needs_shipping() or WC()->cart->get_shipping_packages(), which have no effect on method visibility.

Leave a Comment