The Short Version
- A WordPress plugin fatal error ran every five minutes for nineteen days. It started August 10 and I found it August 29.
- WordPress emailed me the whole time. The notices went to a catch-all inbox and I deleted them unread.
- Nothing visible broke. The site returned 200, checkout worked, and one monthly subscription renewal quietly failed to collect.
- One class check with no null guard caused it. The fixed version was already published, but the plugin does not auto-update.
- Uptime monitoring would not have caught any of it. The failure lived in a cron callback no external check ever touches.
Nineteen Days of "Your Site is Experiencing a Technical Issue"
On August 29 I opened wp-content/debug.log on sitezinc.com and found a 15.47 MB file. It was one WordPress plugin fatal error, repeated roughly every five minutes, going back to August 10. Nineteen days.
The site never went down. The shop kept taking orders. Nothing on the front end looked wrong and no visitor ever saw an error page.
WordPress did tell me. It has done that since version 5.2, with an automated email titled "Your Site is Experiencing a Technical Issue." Those emails went to my admin address every couple of days for nineteen days straight. I deleted every one of them without opening it.
This is my own site, so there is no client to protect and nobody else to blame. Here is the whole thing, including the part where recovering the money was harder than fixing the code.
What Was Actually Failing Every Five Minutes
The site sells a small monthly subscription product at $40 a month, renewing on the tenth. On August 10 at 12:20 PM Mountain the renewal callback fired and hit a fatal error inside the bridge between SUMO Subscriptions and the WooCommerce Stripe gateway.
WP-Cron does not give up on a failed job. It rescheduled and tried again, and kept trying about every five minutes for the next nineteen days. Every attempt wrote a stack trace to debug.log, which is how that file reached 15.47 MB.
None of it was visible from outside. The homepage returned 200. Checkout worked. The only real consequence was that the August billing cycle was never collected, and the only signal was the mail I was throwing away.
WordPress Told Me. I Was Not Listening.
A WordPress plugin fatal error is one of the few failures core will proactively email you about. According to the Make WordPress Core announcement for the feature, WordPress detects the fatal, mails the site administrator, and includes a link into a special recovery mode so you can still log in when a plugin has broken the admin.
The email is deliberately rate limited, so a failure firing every five minutes does not become five thousand messages. Mine arrived about every other day. That cadence is the feature working correctly.
Two things went wrong on my end, and neither one was WordPress.
First, the notice goes to the address in Settings, General, unless you define RECOVERY_MODE_EMAIL in wp-config.php. Mine went to a catch-all address that fills up with hosting notices, renewal reminders and spam. I skim that inbox. I do not read it.
Second, the subject line is generic by design. It reads exactly the same whether a plugin threw something harmless or your billing is silently broken. After a couple that turned out to be nothing, I had already decided the category was noise.
The fix is one line, and it is the first thing I would change on any site where the admin address is a shared catch-all: define( 'RECOVERY_MODE_EMAIL', 'you@yourdomain.com' ); in wp-config.php. Point it somewhere a human reads.
One Class Test With No Null Guard
The bug itself is small and unglamorous, which is how a WordPress plugin fatal error usually starts. One wrong assumption about a class name.
SUMO Subscriptions carries its own Stripe bridge. To charge a renewal it first has to get hold of the live Stripe gateway object. In version 16.3.3 it did that by walking the registered WooCommerce gateways and accepting the one that passed is_a( $gateway, 'WC_Gateway_Stripe' ).
WooCommerce Stripe Gateway 10.9.0 stopped handing back that class. Its own get_main_stripe_gateway() instantiates WC_Stripe_UPE_Payment_Gateway with no conditional branch, and WC_Gateway_Stripe extends WC_Stripe_UPE_Payment_Gateway. So the live object is the parent, the test against the child returns false, and SUMO's lookup returned null.
Line 271 then called prepare_order_source() on null. Fatal, every five minutes, for nineteen days.
The method SUMO wanted is declared on the abstract WC_Stripe_Payment_Gateway that both classes inherit. It was available the entire time. Only the class test was wrong, and there was no null guard behind it to catch the miss.
Worth knowing if you hit this yourself: turning off the optimized checkout experience does not help. That line has no conditional in it.
Click to see the PHP code: the class test that failed, and the fix (9 lines)
// SUMO Subscriptions 16.3.3, get_stripe_instance(). Simplified.
if ( is_a( $gateway, 'WC_Gateway_Stripe' ) ) {
return $gateway;
}
// SUMO Subscriptions 17.7.0, line 64. The parent class is now accepted.
if ( is_a( $gateway, 'WC_Gateway_Stripe' ) || is_a( $gateway, 'WC_Stripe_UPE_Payment_Gateway' ) ) {
return $gateway;
}The Fix Was Already Published. The Plugin Just Did Not Install It.
SUMO 17.7.0 fixes it properly. Line 64 widens the class test to accept the parent, and line 282 adds the missing null guard so an unresolved gateway throws a caught exception instead of taking down the request.
My site sat on 16.3.3 for the entire outage, with an active maintenance contract, because SUMO does not auto-update. It is a marketplace plugin rather than a wordpress.org one, and nothing was ever going to install that release for me. I had to go look.
The other half of the pair moves on its own. WooCommerce Stripe Payment Gateway reports 700,000 active installations in the WordPress Plugin Directory and last shipped on August 31, 2026. It is on 10.9.1 now.
That asymmetry is the real risk. When one side of a two-plugin integration updates itself and the other side waits for you, the gap between them is exactly where this class of bug lives.
What Uptime Monitoring Would Never Have Caught
I monitor uptime on every site I run and I have written about why that matters. It would not have helped here for one second.
Uptime monitoring answers a single question: does the site respond. This site responded correctly for all nineteen days. A WordPress plugin fatal error inside a scheduled cron callback is invisible to every check that looks at the site from the outside, because no outside request ever runs that code.
That is the part worth taking away, and it has nothing to do with SUMO. Background work fails quietly. The only two signals I had were an email I had decided to ignore and a log file nobody was watching.
I did build a workaround while I waited. A snippet in FluentSnippets that resolved the gateway itself, linted against a real PHP binary and tested against a reproduction of the production fatal. It ran for about twelve minutes. Then 17.7.0 shipped and I deleted it the same night, because its fallback unhooked SUMO's renewal callbacks and would have stripped out the graceful decline the new release added. A workaround that outlives the bug becomes the next bug, which is one reason I am careful about where site-level code lives.
What I have not fixed: debug.log has no rotation on this site, and WP_DEBUG is false while PHP's own error log still points at that path. So real errors collect there with nothing to announce them. Checking it is a habit, not an alert. That gap is still open.
The Recovery Had Two Traps of Its Own
Collecting the missed money took longer than fixing the code, and both obstacles are worth writing down because neither one announces itself.
Resuming a paused SUMO subscription reschedules. It does not catch up. When I resumed, SUMO pushed the next due date forward to September 18 and left the overdue August charge sitting there uncollected. It was never going to retry it. If you pause a subscription to stop a failing renewal, you have to go collect the missed cycle yourself.
The Subscription Next Due date field is UTC and silently rejects any date in the past. It prints the raw stored value with no timezone conversion, so an order placed at 12:17 PM Mountain shows as 6:17 pm on the subscription screen. Worse, the save handler only accepts a timestamp greater than the current one. Set it to yesterday and the page reloads, the save looks like it worked, and nothing has happened. No error, no notice, no order note.
I lost a cycle to that before I stopped reading the form and went and read the save handler. To force a renewal you set the date a few minutes into the future in UTC, then confirm the order note actually appears before you sit there waiting on it.
What It Actually Cost
The August renewal was collected on August 29 at 10:57 PM Mountain, at $40, once 17.7.0 was in. The order moved from pending to processing and the renewal count went from 0 to 1.
The log is the proof. That renewal ran the exact code path that had been going fatal every five minutes for nineteen days, and it completed with no fatal at all. Only three harmless deprecation notices from the Stripe plugin's own internals.
The schedule is back on the tenth of the month. Total damage: one billing cycle delayed by nineteen days, a 15.47 MB log file, and an evening. On a site with a hundred subscribers instead of one, the same bug would have been a very different conversation.
What I Would Tell Myself on August 10
Read the boring email.
That is the whole lesson and I am not going to dress it up. WordPress shipped a fatal error notifier into core years ago, it worked exactly as designed, it sent me the message, and I threw it away because the previous few had turned out to be nothing. The detection failure was not technical. I decided in advance, without looking, that an entire category of message was noise.
Two changes are cheap and both are worth making today. Point RECOVERY_MODE_EMAIL at an address you actually read instead of the catch-all in Settings. And for any marketplace plugin that will not auto-update, put a recurring reminder on the calendar to go check it by hand, because nothing else is going to.
I have written before about a revenue bug on a live store that only surfaced because somebody went and looked at the right file. Same shape here. The information was sitting in plain text the entire time. Nobody read it.
Sources
- Fatal Error Recovery Mode in 5.2, Make WordPress Core
- Recovery Mode, WordPress.org Documentation
- WP_Recovery_Mode_Email_Service, WordPress Developer Resources
- WooCommerce Stripe Payment Gateway, WordPress Plugin Directory




0 Comments