A client site started turning itself off.
Not the site — the plugins. All of them. Every morning I’d get a report that something was broken, log in, and find the plugins list completely deactivated. Turn them back on, everything works, site’s fine all day. Next morning, off again. Somewhere between 3 and 6 AM, consistently, every night.
This went on longer than I want to admit. It took several sessions across multiple days to actually solve, and the answer turned out to be two unrelated problems stacked on top of each other, neither of which had anything to do with the plugins that were being deactivated.
I’m writing it up because the diagnosis is genuinely useful — mass deactivation is a symptom with a small number of possible causes and almost no good documentation — and because this specific site is the reason I ended up building a plugin.
What it wasn’t
The obvious suspects went first.
Not a security plugin. No security plugin was configured to deactivate anything, and the timing didn’t match any scan schedule.
Not a hack. No modified core files, no unexpected admin users, no injected content. And a compromise that deactivates every plugin nightly and does nothing else would be a strange compromise.
Not a cron job or a backup script. Nothing on the server was scheduled in that window that touched WordPress.
Not a PHP fatal on activation. WordPress will deactivate a single plugin that fatals, but it deactivates that plugin, and it tells you. This was all of them, silently.
What all of those explanations have in common is that they assume something is deciding to deactivate plugins. That framing was the problem.
WordPress doesn’t store a list of “deactivated” plugins. It stores a list of active ones, in a single row in wp_options, under the key active_plugins, as a serialized array. Every plugin on that site going inactive at once doesn’t require anything to deactivate them. It requires that one row to get overwritten with something wrong.
Once I was looking for “what is rewriting active_plugins” instead of “what is deactivating plugins,” the investigation got much more productive.
(Worth noting: this site used a non-standard table prefix — wpfp_ rather than wp_. If you’re poking around in a database and can’t find the tables you expect, check wp-config.php before you conclude anything is missing.)
Cause one: a table with a missing column, and Google
The site’s debug.log was enormous, and it was full of database errors from the Redirection plugin. Its 404 logging table was missing a domain column — a column the current version of the plugin expected to exist. Almost certainly the residue of a partial or failed upgrade at some point in the past.
So every time Redirection tried to log a 404, the insert failed and threw a database error.
The volume is the part that matters. Googlebot was crawling the site and hitting a large number of URLs that no longer existed. Every one of those 404s triggered a write attempt against a broken table, and every one of those failed. Thousands of failed writes, concentrated in the overnight hours when the crawler was most active — which is exactly the window when the deactivations were happening.
The fix: rebuild the table. Not an ALTER to add the missing column — I dropped it and recreated it with a full CREATE TABLE statement matching what the current plugin version expects, in phpMyAdmin. When a table’s schema has drifted this far from what the code expects, patching one column just leaves you wondering what else is wrong.
That should have been it.
It wasn’t. The next morning, plugins were off again.
Cause two: a collation mismatch, and a cascade
Back into the log, now much quieter with the Redirection errors gone, and a second class of error was visible underneath: Illegal mix of collations.
Two of Rank Math Pro’s analytics tables were on utf8mb4_unicode_ci. The rest of the database was on utf8mb4_unicode_520_ci.
If you haven’t run into this: collation is the ruleset MySQL uses to compare and sort text. Two tables on different collations can each be perfectly valid on their own, and then any query that joins them or compares a text column across them fails outright. Not a warning. A hard error.
This is a nasty class of bug because nothing is broken until something tries to join those tables. The site works. The tables have data in them. Then one query pattern touches both, and it fails every time, forever.
How that turns into deactivated plugins
Here’s the chain, and it’s the part I’d never have guessed:
Rank Math Pro’s analytics run on Action Scheduler — the background job library that WooCommerce, Rank Math, and a lot of other plugins share. Action Scheduler queues jobs and runs them on cron, overnight, in bulk.
Those jobs were hitting the collation mismatch and failing. Failing, retrying, requeuing, failing again — a cascade of failed background jobs compounding through the night.
And somewhere in that cascade, wp_options was being written to in a state where the active_plugins row got rewritten. The serialized array came back wrong, and WordPress read it the only way it can read a malformed array: as fewer active plugins.
Nothing deactivated anything. A background job cascade corrupted one row in one table, and the plugins list is that row.
The fix, in three parts:
ALTER TABLE ... CONVERT TO CHARACTER SETon the two mismatched Rank Math tables, bringing them in line with the rest of the database- Deactivated Rank Math Pro
- Waited
Next morning: plugins still on. And the morning after that. Fixed.
The part that turned into a product
Rank Math Pro’s license had lapsed.
That’s why its tables were in that state. An unlicensed premium plugin stops receiving updates, and it sits there running code written for an older WordPress and an older PHP against a database that has moved on around it. Nothing announces this. There’s no warning that says “this plugin has been unsupported for eight months and its schema no longer matches the environment.” It just quietly becomes a liability.
That was the moment the plugin idea stopped being an idle thought.
Because look at what it actually took to find this:
- Reading a debug log large enough to be genuinely painful to open
- Recognizing “Illegal mix of collations” as a schema problem rather than a content problem
- Knowing that Action Scheduler runs overnight in bulk
- Knowing that
active_pluginsis a single serialized row and therefore a single point of failure - Connecting a lapsed license to a table schema drifting out of sync
- Correlating crawler activity with the timing of the failures
None of that is exotic knowledge. But all of it is diagnostic knowledge, and none of the monitoring tools I had installed surfaced any of it. Uptime monitors said the site was up — it was. Security plugins found nothing — there was nothing to find. The debug log had the answer in it the whole time, buried in tens of thousands of lines of noise.
So I built the thing I’d needed: something that watches for broken table schemas and collation mismatches specifically, watches active_plugins for unexpected rewrites, detects 404 floods, filters the debug log down to what actually matters, and — directly because of this site — flags premium plugins whose licenses have lapsed and whose code is drifting away from the environment it’s running in.
Every module in it exists because of a night I spent staring at something like this.
If your plugins are deactivating themselves
The short version of what to check:
Look at active_plugins in wp_options first. That single row is your plugins list. If it’s being rewritten, everything else you’re investigating is downstream.
Read the debug log, but read it for patterns, not lines. The signal here was volume and repetition — thousands of identical failures — not any one dramatic error.
Take “Illegal mix of collations” seriously. It means two tables disagree about their text ruleset, and it will break any query that spans them. It’s invisible until it isn’t.
Check what’s running on Action Scheduler. If a background job library is failing in a loop overnight, look at what those jobs touch.
Check your premium plugin licenses. A lapsed license isn’t just missing features. It’s frozen code aging against a moving environment.
And check the table prefix before you conclude a table is missing. I’ve wasted time on that one more than once.
Canary Site Monitor is free on WordPress.org. It exists because of the night described above.