Guide

WordPress PHP warning spike — finding the plugin or file causing the flood

Yesterday your PHP error log was 4 MB. This morning it is 1.2 GB and still growing. You SSH in, run du -sh /var/log/php-fpm/, and watch the number tick upward in real time.

1. Problem

Yesterday your PHP error log was 4 MB. This morning it is 1.2 GB and still growing. You SSH in, run du -sh /var/log/php-fpm/, and watch the number tick upward in real time. The site loads — slowly — and the homepage looks fine, but the error log is filling at roughly 30 MB per minute.

You are searching "wordpress error log filling up warnings" because nothing crashed and nothing is obviously broken, but something is screaming inside the server.

A quick tail shows the same warning, repeating, thousands of times:

[27-Apr-2026 09:14:02 UTC] PHP Warning:  Undefined array key "subscription_id" in /var/www/html/wp-content/plugins/some-membership-plugin/includes/class-renewal.php on line 88
[27-Apr-2026 09:14:02 UTC] PHP Warning:  Undefined array key "subscription_id" in /var/www/html/wp-content/plugins/some-membership-plugin/includes/class-renewal.php on line 88
[27-Apr-2026 09:14:02 UTC] PHP Warning:  Undefined array key "subscription_id" in /var/www/html/wp-content/plugins/some-membership-plugin/includes/class-renewal.php on line 88

Same file. Same line. Tens of thousands of times an hour. You did not push code. The plugin was last updated six hours ago by auto-updates. This is the classic profile of a sudden flood of PHP warnings: stable for weeks, then a vertical wall on the rate graph.

This guide is about that rate jump — not a planned PHP version upgrade. It is about the case where a single code path, on a real production site, started misfiring overnight.

2. Impact

A warning spike is not "log noise." It is a tax on every layer of your stack.

  • Disk fills fast. At 30 MB/minute, a 50 GB volume is full in under a day. When the disk fills, MySQL stops writing and PHP-FPM stops accepting requests. The site goes down for a reason unrelated to the actual bug.
  • Slow requests. Every error_log() call hits disk. A warning inside a tight loop means thousands of synchronous writes per request. Page generation triples.
  • Real errors get buried. When the log is 99.9% the same warning, the genuinely new error — the php.fatal, the SQL deadlock, the failed checkout — is impossible to find by eye.
  • Backup and log shipping break. Log rotation runs out of inodes. Forwarders fall behind, then drop entire batches.
  • Cost. If you ship logs to CloudWatch or Datadog, an extra 100 GB/day hits the bill within a week.

The site is not down. It is degrading silently while one line of code emits the same warning a hundred times per second. That is what makes rate-of-change the signal worth alerting on.

3. Why It’s Hard to Spot

A warning flood is the most-missed silent failure in the WordPress catalogue.

  1. HTTP status is 200. The page rendered. Pingdom is green, UptimeRobot is green, the host's "site health" is green. The warning never crossed the wire.
  2. The dashboard shows nothing. WordPress's Site Health tool checks PHP version and a few constants. It does not read the error log or measure log volume.
  3. WP_DEBUG is off in production. Warnings go to error_log only. Nothing surfaces in the UI. No customer will say "your warning rate jumped 40x at 03:14."
  4. Volume looks like absence of a problem. Operators ignore the error log because it always has noise. A 40x jump still looks like noise to the eye until disk fills.
  5. Auto-updates run while you sleep. Core, plugins, and themes auto-update by default. The change happened at 02:37 UTC. The spike started at 02:38. You woke up at 08:00.

The result: "wordpress sudden flood of php warnings" is almost always discovered by accident — disk full, slow page, a glance at the log — hours or days after it started.

4. Cause

The wp_php_warnings_total signal is a counter. The Logystera WordPress plugin hooks PHP's error handler and increments this counter every time the request hits an E_WARNING, E_USER_WARNING, E_NOTICE, or E_USER_NOTICE. Each emission carries labels: error_type, file, line, and the originating page.

Under normal operation on a healthy site, rate(wp_php_warnings_total[5m]) sits in the low single digits per second. A handful of legacy plugins emit a notice or two per cron tick, and that's it. The shape is flat.

A spike means one of three things is true at the code level:

  • A code path that used to be cold is suddenly hot. A hook fires more often than it used to (a new background job, a new shortcode rendered on a high-traffic page, a feed import that started running every minute instead of every hour).
  • A code path that used to succeed is suddenly failing soft. An array key that used to be present is now missing because an upstream API changed shape, or a database column was renamed, or a transient was cleared, or an option got reset. PHP would have crashed in the strict-types era, but with a warning it just emits and continues.
  • A new code path was just deployed. A plugin auto-update, a theme update, or a wp.state_change 6 hours ago shipped code with an undeclared variable, a deprecated each(), a null passed where an object is expected, or a missing isset() check. The warning fires on every request that reaches the new code.

The rule wp_php_warning_spike watches rate(wp_php_warnings_total[5m]) against the same metric over the previous 6 hours. When the short-window rate exceeds the long-window rate by a configured factor (typically 5x or more) and stays elevated, the rule fires. That ratio — short-window / long-window — is the entire detection. It does not care about absolute volume. A site that emits 0.1 warnings/sec normally and suddenly emits 5/sec is just as alertable as a site that goes from 10/sec to 500/sec.

The wp_state_changes_total signal is the second half of the picture. When a plugin updates, deactivates, activates, or its files change on disk, the WP plugin emits a wp.state_change event. Spikes in wp_php_warnings_total that begin within an hour of a wp.state_change are almost always caused by that change.

5. Solution

5.1 Diagnose (logs first)

Stop trying to read the whole log. The log is the wrong shape for that. You need to compress the flood into the small set of unique warnings driving it, then bisect from there.

Step 1 — Confirm the rate is actually elevated.

ls -lh /var/log/php-fpm/www-error.log
# -rw-r--r-- 1 root adm 1.2G Apr 27 09:14 www-error.log

A multi-GB error log on a site that was fine yesterday is the rate spike. Each line of that file is one wp_php_warnings_total increment. Anything more than ~5 MB per hour on a small site is suspicious; ~5 MB per minute is the spike profile.

Step 2 — Collapse duplicates and find the dominant warning.

This is the single most useful command in this guide:

grep -i "PHP Warning\|PHP Notice\|PHP Deprecated" /var/log/php-fpm/www-error.log \
  | sed -E 's/^\[[^]]+\] //' \
  | sed -E 's/[0-9]+/N/g' \
  | sort | uniq -c | sort -rn | head -10

The first pass strips timestamps. The second normalizes line numbers and IDs into N so identical-looking warnings collapse. The output looks like:

 187234 PHP Warning: Undefined array key "subscription_id" in /var/www/html/wp-content/plugins/some-membership-plugin/includes/class-renewal.php on line N
   1421 PHP Warning: Trying to access array offset on null in /var/www/html/wp-content/themes/your-theme/inc/related-posts.php on line N
     88 PHP Notice: WP_Query was called with an argument that is deprecated since version 6.4.0 in /var/www/html/wp-includes/functions.php on line N

187,234 hits from one file is your spike. Everything else is background noise. Each of those lines is a wp_php_warnings_total event with file= and line= labels. The dominant one names the plugin, file, and roughly the code path.

Step 3 — Correlate with wp.state_change.

If the plugin emits the wp_state_changes_total signal, query it for the last 24 hours:

grep "wp.state_change" /var/log/wordpress/audit.log | tail -20

You're looking for a plugin_updated, plugin_activated, or theme_updated event whose timestamp aligns (within an hour) with the start of the spike. Nine times out of ten this names the change responsible.

Step 4 — Find which pages trigger it most.

The wp_top_error_pages signal labels warnings by request URI. If you don't have it, the access log gives you the same answer:

grep -E "$(date +%d/%b/%Y)" /var/log/nginx/access.log \
  | awk '{print $7}' | sort | uniq -c | sort -rn | head -20

Cross-reference with the warning's stack. If the dominant page is /?wc-ajax=update_order_review and the warning is in a membership plugin's renewal class, you've found the request path that triggers the bug.

Step 5 — Pin it to the line.

Once you know the file:

sed -n '80,100p' /var/www/html/wp-content/plugins/some-membership-plugin/includes/class-renewal.php

You're looking for the unguarded array access, the missing isset(), the null dereference, or the function call against an upstream that changed shape. Each wp_php_warnings_total increment came from this exact line.

5.2 Root Causes

(see root causes inline in 5.3 Fix)

5.3 Fix

Multiple root causes, in order of frequency. Each maps cleanly back to a signal pattern.

1. Plugin or theme update introduced an unguarded code path.

Signal pattern: wp_state_changes_total event for plugin_updated, immediately followed by wp_php_warnings_total rate jump on a file inside that plugin. Roughly 60% of spike incidents.

Fix: roll back the plugin while you investigate.

wp plugin install some-membership-plugin --version=4.2.1 --force

File an issue with the plugin author quoting the warning text. Patch the file — do not silence with error_reporting(E_ALL & ~E_WARNING), that mask is too broad.

2. Upstream API or external service changed payload shape.

Signal pattern: wp_php_warnings_total spike with no preceding wp_state_changes_total. Plugin code is unchanged; what changed is what it received. Common with payment gateways, syndication feeds, remote inventory APIs.

Fix: identify the network call near the failing line. Add isset() / array_key_exists() guards. Log the actual payload once, write defensive code against it.

3. Database row or option got reset / corrupted.

Signal pattern: wp_php_warnings_total spike on a function that reads a specific option, with no plugin update. Often follows a restore-from-backup, a search-replace migration, or a manual SQL touch.

Fix:

SELECT option_name, LENGTH(option_value) FROM wp_options WHERE option_name LIKE 'some_plugin_%';

Re-seed defaults from the plugin's activation hook.

4. Cron or background job hot loop.

Signal pattern: wp_php_warnings_total rate aligns with a fixed interval (every 5 min, every hour) — same warning fires in lockstep with WP-Cron.

Fix: identify the scheduled hook (wp cron event list), find the owning plugin, patch the handler.

5. Memory pressure pushing safe code into warning territory.

Signal pattern: wp_php_warnings_total spike correlated with wp_php_fatal increase (Allowed memory size exhausted). Warnings about null returns appear because object construction is failing under pressure.

Fix: this is a memory issue masquerading as a warning issue. Raise memory_limit, profile the page. The fatal is the real signal.

5.4 Verify

You cannot trust "the page loads." It loaded yesterday too. The verification is in the rate, and only the rate.

After the fix:

  1. Watch the counter flatten. rate(wp_php_warnings_total[5m]) should return to its baseline (typically the same range it sat in before the spike) within 10 minutes of the fix landing. If it doesn't, you fixed a different warning.
  1. Confirm the dominant entry disappears. Run the same dedup grep from section 5:
   tail -10000 /var/log/php-fpm/www-error.log \
     | grep -i "PHP Warning" \
     | sed -E 's/^\[[^]]+\] //; s/[0-9]+/N/g' \
     | sort | uniq -c | sort -rn | head -5

The 187,234-count line should be gone, or down to single digits caused by stale OPcache copies.

  1. Truncate the log so the spike doesn't keep distorting your dedup output.
   sudo truncate -s 0 /var/log/php-fpm/www-error.log
   sudo systemctl reload php-fpm
  1. Time window: 30 minutes under representative traffic. If wp_php_warnings_total stays at baseline through one full traffic cycle (peak hour included) and no wp_php_fatal follows, the fix is good. If the rate is still elevated after 30 min, you patched a symptom — go back to step 2 of section 5 and find the next dominant entry.
  1. Confirm no new wp_state_changes_total. If a plugin re-updated itself overnight via auto-update and reverted your patch, the spike will return.

Healthy in logs looks like: a few warnings per minute, varied (different files, different lines, different messages). Unhealthy looks like: one warning, repeated, dominating the dedup output.

6. How to Catch This Early

Fixing it is straightforward once you know the cause. The hard part is knowing it happened at all.

This issue surfaces as wp_php_warnings_total.

The hard part of this failure is not fixing the missing isset(). The hard part is knowing the spike started six hours ago instead of finding out tomorrow when the disk fills.

Almost no monitoring catches it:

  • HTTP returns 200, so uptime checks miss it.
  • Page loads, so synthetic monitoring misses it.
  • WordPress doesn't read its own error log.
  • WP_DEBUG is off, so display-side detection is impossible.
  • The error existed before the spike, so naive thresholds don't catch it.

The only signal that catches it cleanly is rate of change. A 40x jump in wp_php_warnings_total over a 5-minute window, sustained, is unambiguous. That is exactly what the wp_php_warning_spike rule evaluates: short-window rate against the trailing baseline, with a configurable multiplier and minimum sustain.

Logystera ingests wp_php_warnings_total, derives the rate, compares against the rolling baseline, and fires wp_php_warning_spike the first minute the threshold breaks. The alert links the dominant error_type + file + line, the correlated wp_state_changes_total event, and the top error pages from wp_top_error_pages. You wake up to "spike on class-renewal.php:88, started 02:38, plugin updated 02:37," not "disk full, site down."

The fix is fast once you know. The detection is the entire problem.

7. Related Silent Failures

If the warning spike pattern is familiar, these adjacent failures probably are too. Each is detected by the same general approach: derive a rate from a counter signal, compare to baseline, alert on deviation.

  • WordPress fatal error after plugin update — when wp_php_warnings_total is the early signal and wp_php_fatal is the consequence five minutes later. If the warning fires inside a code path that also runs at request-init, the next deploy promotes it from warning to fatal.
  • PHP deprecation flood after PHP 8 upgrade — the planned-upgrade cousin of this guide. Same wp_php_warnings_total signal, but error_type=deprecation and triggered by a wp.environment change, not a plugin update.
  • WordPress plugin silently activated — a wp_state_changes_total event with no operator action. Often the prelude to a warning or fatal spike from the freshly-activated code.
  • WordPress memory limit warning before crashphp.warning with Allowed memory size of N bytes appears before the same condition becomes a wp_php_fatal. The warning is the early window.
  • WordPress error log filling disk — the operational consequence when the spike runs unmonitored long enough. Disk fills, MySQL halts, the site goes down for a reason that no longer points at the original warning. Catching the rate spike upstream prevents this entirely.

See what's actually happening in your WordPress system

Connect your site. Logystera starts monitoring within minutes.

Logystera Logystera
Monitoring for WordPress and Drupal sites. Install a plugin or module to catch silent failures — cron stalls, failed emails, login attacks, PHP errors — before users report them.
Company
Copyright © 2026 Logystera. All rights reserved.