Commit 56d0a8dc9e1 for woocommerce
commit 56d0a8dc9e140f95e753bb1bd7db9bae35a6cf1c
Author: Seun Olorunsola <30554163+triple0t@users.noreply.github.com>
Date: Tue Mar 31 12:25:30 2026 +0100
Performance: lazy-load _used_by meta in WC_Coupon to fix memory exhaustion on high-usage coupons (#63755)
* Performance: lazy-load _used_by meta in WC_Coupon to fix memory exhaustion on high-usage coupons
On stores where a coupon has been used hundreds of thousands of times,
WC_Coupon::__construct() was calling get_post_meta($id, '_used_by') which
returns every usage row as a PHP array. This caused hundreds of MB of memory
allocation on every WC_Coupon instantiation — including order status changes,
cart validation, and admin edits that never need the used_by list.
Fix: defer the _used_by fetch until get_used_by() is actually called. The
default is now null (not loaded) instead of an empty array. get_used_by()
populates $this->data['used_by'] directly on first access, bypassing
set_prop() so the lazy fetch does not mark the object dirty. get_data() also
triggers the lazy load before returning the raw data array, so REST API
responses continue to include the full used_by list.
* Fix: use edit context in get_data() to avoid filter recursion in WC_Coupon::get_used_by()
diff --git a/plugins/woocommerce/changelog/wooplug-6370-wc_coupon-construct-and-_used_by-meta b/plugins/woocommerce/changelog/wooplug-6370-wc_coupon-construct-and-_used_by-meta
new file mode 100644
index 00000000000..b9371c6fcac
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooplug-6370-wc_coupon-construct-and-_used_by-meta
@@ -0,0 +1,4 @@
+Significance: patch
+Type: performance
+
+Lazy-load _used_by post meta in WC_Coupon to avoid loading all usage records into memory on construction.
diff --git a/plugins/woocommerce/includes/class-wc-coupon.php b/plugins/woocommerce/includes/class-wc-coupon.php
index 51ecd525958..32caf04a12c 100644
--- a/plugins/woocommerce/includes/class-wc-coupon.php
+++ b/plugins/woocommerce/includes/class-wc-coupon.php
@@ -50,7 +50,7 @@ class WC_Coupon extends WC_Legacy_Coupon {
'minimum_amount' => '',
'maximum_amount' => '',
'email_restrictions' => array(),
- 'used_by' => array(),
+ 'used_by' => null,
'virtual' => false,
);
@@ -152,6 +152,9 @@ class WC_Coupon extends WC_Legacy_Coupon {
* @return array
*/
public function get_data() {
+ // Ensure used_by is populated before the raw data array is returned, since
+ // parent::get_data() reads $this->data directly and bypasses get_used_by().
+ $this->get_used_by( 'edit' );
$data = parent::get_data();
if ( '' === $data['minimum_amount'] ) {
$data['minimum_amount'] = '0';
@@ -455,11 +458,22 @@ class WC_Coupon extends WC_Legacy_Coupon {
/**
* Get records of all users who have used the current coupon.
*
+ * The list is loaded lazily on first access rather than during object construction,
+ * because a coupon with hundreds of thousands of usages would otherwise allocate an
+ * enormous PHP array on every WC_Coupon instantiation — even for code paths such as
+ * order-status changes or cart validation that never need this data.
+ *
* @since 3.0.0
* @param string $context What the value is for. Valid values are 'view' and 'edit'.
* @return array
*/
public function get_used_by( $context = 'view' ) {
+ if ( is_null( $this->data['used_by'] ) ) {
+ // Bypass set_prop() so the lazy fetch does not mark the object as dirty.
+ $this->data['used_by'] = $this->get_id()
+ ? array_filter( (array) get_post_meta( $this->get_id(), '_used_by', false ) )
+ : array();
+ }
return $this->get_prop( 'used_by', $context );
}
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-coupon-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-coupon-data-store-cpt.php
index e8e3a41d6a6..86c909651c8 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-coupon-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-coupon-data-store-cpt.php
@@ -144,7 +144,6 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat
'minimum_amount' => get_post_meta( $coupon_id, 'minimum_amount', true ),
'maximum_amount' => get_post_meta( $coupon_id, 'maximum_amount', true ),
'email_restrictions' => array_filter( (array) get_post_meta( $coupon_id, 'customer_email', true ) ),
- 'used_by' => array_filter( (array) get_post_meta( $coupon_id, '_used_by' ) ),
)
);
$coupon->read_meta_data();