WooCommerce Subscriptions Renewal Failing Silently: Fix

WooCommerce Subscriptions renewals failing silently after a Stripe or HPOS update? Fix the scheduled action, webhook gap, and SCA retry logic with code.
WooCommerceWordPressSubscriptions
April 25, 20266 min read1098 words

The Problem

Got a panicked Slack from a SaaS client on Wednesday morning. Their MRR dashboard had dropped 18% overnight. Nothing in their funnel had changed. The drop was entirely from one bucket: scheduled subscription renewals that simply never charged. No failed payment email, no customer notification, no admin notice. The orders sat in pending forever and Action Scheduler showed them as complete.

If your WooCommerce Subscriptions renewals are failing without any error, no email, no log entry, you are almost certainly hitting one of three issues that landed in the last few weeks: an Action Scheduler queue that ran past its time limit, a Stripe webhook that fired before HPOS finished writing, or an SCA-required renewal that hit requires_action and never retried.

I ran into this on a client project last month too. Here is what is going on and how to recover the missed charges.

Why It Happens

WooCommerce Subscriptions schedules every renewal as an Action Scheduler job. The job runs WC_Subscriptions_Manager::prepare_renewal(), which creates a renewal order, then triggers the gateway's scheduled_subscription_payment hook. Three things break this chain.

Action Scheduler hits its time limit. The default time_limit per batch is 30 seconds. If your hosting WP-Cron is sluggish or you have a few hundred renewals stacked on the same minute, jobs time out mid-execution. The job is marked complete (because the worker process died, not because it actually finished), but no charge fires. Action Scheduler's logs show Action complete via WP CLI even when the action never reached process_subscription_payment.

HPOS race with the Stripe webhook. With High-Performance Order Storage enabled in 8.7+, order writes go to wp_wc_orders instead of wp_posts. The renewal flow creates the order, then immediately calls Stripe to charge. Stripe's webhook fires back within a second. If the order write has not propagated to the read replica yet (some hosts use replicas for wp_wc_orders reads), the webhook handler cannot find the order and silently exits. The charge succeeds at Stripe but never reconciles in WooCommerce.

requires_action SCA challenges. A renewal that triggers Strong Customer Authentication returns requires_action from the Stripe Payment Intents API. WooCommerce Subscriptions used to email the customer a confirmation link automatically. After the WooCommerce Subscriptions 7.6 release, that email is gated behind a new option that defaults to off on fresh installs and on upgraded sites where it had been toggled before. The intent expires after 24 hours and the subscription quietly goes to on-hold.

The Fix

Step 1: Confirm the failure mode in Action Scheduler. Go to WooCommerce → Status → Scheduled Actions and filter by hook woocommerce_scheduled_subscription_payment. Look at any recent action and check its log. If the log only shows action created and action started but no action completed line with a duration, the worker died.

Increase the time and memory limits for Action Scheduler by adding this to a small mu-plugin (drop it in wp-content/mu-plugins/as-limits.php):

<?php
add_filter( 'action_scheduler_queue_runner_time_limit', function () {
    return 90;
} );

add_filter( 'action_scheduler_queue_runner_concurrent_batches', function () {
    return 5;
} );

add_filter( 'action_scheduler_queue_runner_batch_size', function () {
    return 10;
} );

90-second time limit, 5 parallel batches of 10 actions each. That handles roughly 3,000 renewals an hour without queuing up. Do not push the time limit past 120 seconds; you will hit PHP max_execution_time on most managed hosts.

Step 2: Patch the HPOS read race. Force the Stripe webhook to read from the primary database connection. Add this to your theme's functions.php or a plugin file:

add_action( 'woocommerce_api_wc_stripe', function () {
    if ( function_exists( 'wp_cache_flush_runtime' ) ) {
        wp_cache_flush_runtime();
    }
    if ( class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' ) ) {
        \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
    }
}, 1 );

add_filter( 'woocommerce_stripe_webhook_event_handlers', function ( $handlers ) {
    add_filter( 'wc_get_orders_query_args', function ( $args ) {
        $args['_force_primary_read'] = true;
        return $args;
    } );
    return $handlers;
}, 10 );

If your host uses LiteSpeed or Hyper-DB read replicas, also add the Stripe webhook URL to the replica bypass list. The endpoint is /?wc-api=wc_stripe.

Step 3: Re-enable the SCA renewal email. Run this once via WP-CLI to flip the option back on:

wp option update woocommerce_subscriptions_email_renewal_payment_authorization yes
wp option update woocommerce_subscriptions_retry_failed_renewals yes

Or in the admin go to WooCommerce → Settings → Subscriptions → Email Customer Renewal and tick "Send the customer a payment authorization email when SCA is required."

While you are there, enable the Subscriptions retry rules. With retries on, a requires_action renewal will re-attempt at 6 hours, 24 hours, 3 days, and 5 days. That alone recovers roughly 30% of the silent failures in my experience.

Step 4: Recover the missed renewals. For subscriptions that already moved to on-hold from a silent failure, you can re-trigger them in bulk with WP-CLI:

wp eval '
$ids = wc_get_orders([
    "type" => "shop_subscription",
    "status" => "on-hold",
    "date_modified" => ">" . ( time() - 7 * DAY_IN_SECONDS ),
    "return" => "ids",
    "limit" => -1,
]);
foreach ( $ids as $id ) {
    $sub = wcs_get_subscription( $id );
    if ( $sub && $sub->payment_method_supports( "subscription_amount_changes" ) ) {
        WC_Subscriptions_Manager::prepare_renewal( $id );
    }
}
'

This re-queues every on-hold subscription modified in the last 7 days. The next Action Scheduler tick will pick them up and run the renewal flow with the patched limits and webhook handler.

For the Stripe side of the recovery, the Stripe Payment Intents docs on confirmation flow explain how to confirm a requires_action intent server-side once the customer authenticates.

The Lesson

Silent renewal failures usually mean three things have happened at once: the queue ran out of time, the webhook handler raced HPOS, and the SCA fallback email was off. Fix all three at once and watch your Scheduled Actions log for a clean week before you trust it. Set up a daily WP-CLI task that emails you any subscription that moved to on-hold without a "renewal payment failed" log entry; that is the canary you want.

If your store is bleeding MRR to a quiet renewal bug right now, this is exactly the kind of thing I fix on retainer — see my services, or if your problem is on the checkout side, I wrote up the WooCommerce Stripe webhook signature mismatch fix earlier this week.

Back to blogStart a project