WooCommerce Cart Block Shows Wrong Shipping After Zone Overlap

WooCommerce 10.8 cart block returning the wrong shipping rate when two zones overlap on the same postcode? Here is why the picker grabs the wrong zone and the exact fix.
WooCommerceShippingCart Block
June 16, 20266 min read1105 words

The Problem

A client store running WooCommerce 10.8 on the cart and checkout blocks started getting refund requests from UK customers. The invoices showed a flat 4.99 shipping fee for orders that should have qualified for a 9.99 express rate in their postcode. The Store API was returning the wrong zone, not the customer.

I reproduced it in staging. Two overlapping zones: a national "UK Standard" zone matching GB with a flat 4.99 rate, and a more specific "London Same-Day" zone matching the EC*, WC*, and E1* postcodes with a 9.99 express rate. A cart shipping to EC1A 1BB should have surfaced both options. The Store API returned only the 4.99 national rate. The classic shortcode cart on the same store, same product, same address, returned both.

The Network panel told the rest of the story. The wc/store/v1/cart response had a shipping_rates array with a single entry, and the package_id referenced the wrong zone:

"shipping_rates": [
  {
    "package_id": 0,
    "name": "Shipping",
    "destination": { "postcode": "EC1A 1BB", "country": "GB" },
    "shipping_rates": [
      { "rate_id": "flat_rate:3", "name": "UK Standard", "price": "499" }
    ]
  }
]

The express rate was never offered. The customer paid the wrong fee. Multiply that across a few hundred orders a week and the support load gets ugly.

Why It Happens

WooCommerce evaluates shipping zones in the order they appear in WC_Shipping_Zones::get_zones(), which sorts by zone_order ascending. The first zone whose location rules match the destination wins. Methods from later matching zones are not added to the package, they are discarded.

That has always been the behaviour. The reason the cart block exposes it more often is that the Store API resolves the address from the cart payload before the customer has typed anything into the shipping form. If the only data it has is a default country, it matches the broadest zone, picks its rates, and caches the package. When the customer enters a postcode, the cart block sends an update_customer request, but a regression in the 10.8 hydration path skips the package rebuild when the country has not changed. The cached package from the country-only resolution sticks.

You can see the same pattern in any store where a national zone sits above a region-specific zone in the zone order. The bug is older than 10.8, the cart block just made it cheap to hit because the API response is the source of truth for the new checkout UI. There is no fallback to the classic calculation.

The Store API package documentation confirms each package is matched to exactly one zone. There is no notion of merging methods from multiple matching zones.

The Fix

Three steps. Reorder the zones, force a package rebuild on postcode change, and verify with the Store API directly.

Step 1: Reorder the zones so the most specific one is first. Zones are checked top-down. The London zone with postcode rules must sit above the national zone with only the country rule. From WooCommerce → Settings → Shipping → Shipping Zones, drag the more specific zone to the top of the list. If you have many regional zones, the rule is: postcode-specific above region-specific above country-only above "Locations not covered".

If you cannot reorder by hand because of a bulk import, run this once in WP-CLI:

wp db query "UPDATE wp_woocommerce_shipping_zones SET zone_order = 1 WHERE zone_name = 'London Same-Day';"
wp db query "UPDATE wp_woocommerce_shipping_zones SET zone_order = 2 WHERE zone_name = 'UK Standard';"
wp cache flush

wp cache flush matters. WooCommerce caches the zone list in object cache and the change will not show in the API response until the cache is cleared.

Step 2: Force the cart block to rebuild the package on postcode change. The 10.8 hydration path treats the package as stable when the country is unchanged. That is wrong for any store with postcode-based zones. Drop this in a mu-plugin or your theme's functions.php:

add_filter(
    'woocommerce_store_api_cart_update_customer_from_request',
    function ( $customer, $request ) {
        $postcode = $request->get_param( 'shipping_address' )['postcode'] ?? null;
        if ( $postcode && $postcode !== $customer->get_shipping_postcode() ) {
            WC()->cart->calculate_shipping();
            WC()->cart->calculate_totals();
        }
        return $customer;
    },
    10,
    2
);

The filter fires after the customer's address is updated. If the postcode changed, we force a fresh shipping calculation so the zone matcher runs against the new address. The cart block reads the rebuilt rates on its next response.

Step 3: Verify with the Store API directly. Do not rely on the UI for the smoke test. Hit the API with a real postcode and read the response:

curl -s -X POST \
  https://example.com/wp-json/wc/store/v1/cart/update-customer \
  -H 'Content-Type: application/json' \
  -d '{
    "shipping_address": {
      "country": "GB",
      "postcode": "EC1A 1BB"
    }
  }' | jq '.shipping_rates[0].shipping_rates[] | {name, price}'

You should see both rates returned:

{ "name": "London Same-Day", "price": "999" }
{ "name": "UK Standard", "price": "499" }

If only one comes back, the zone order is still wrong or the cache did not clear. Re-run the WP-CLI commands and try again.

One last thing. If the store uses Action Scheduler for backorders, run wp action-scheduler run --hooks=woocommerce_shipping_zone_method_updated after reordering. A pending job from before the change can re-cache the old order and the bug returns the next time a logged-in customer with a stale session loads the cart.

The Lesson

Overlapping zones in WooCommerce do not merge. The matcher picks the first one in order and stops. With the cart block, the Store API package gets cached against whatever it could match first, and a hydration shortcut in 10.8 means the package does not always rebuild when the postcode changes. Put the most specific zone at the top, force a recalc on postcode change, and verify with curl, not the UI.

If your store has the same bug and the support tickets are stacking up, that is a project I get paid to unstick. See my services. For a related cart block issue that ate hours on the same project, read WooCommerce coupon not applying in checkout block.

Need a WooCommerce store that bills the right rate every time? Get it shipped.

Back to blogStart a project