You click “Update plugin”. The spinner spins. Then WordPress faceplants with: “Fatal error: Maximum execution time of 30 seconds exceeded”. Or the page just goes white and leaves you staring at your own reflection like it owes you money.
This error isn’t WordPress being dramatic. It’s your server enforcing a hard stop: a PHP request ran longer than allowed. The trick is fixing it without “just set it to 300 seconds” and calling it a day—because long timeouts are how small problems become production incidents.
What the error actually means (and what it doesn’t)
In most WordPress stacks, “Max execution time exceeded” is thrown by PHP when a request runs longer than max_execution_time. That’s a PHP limit (per request) designed to prevent runaway code from hogging CPU indefinitely.
But in real systems, PHP’s own limit is rarely the only timeout in play. You usually have a stack of timers:
- Browser / client timeout (user gives up, or reverse proxy closes connection).
- Reverse proxy timeouts (Nginx, CDN, load balancer).
- Web server timeouts (Apache, Nginx fastcgi).
- PHP-FPM timeouts (
request_terminate_timeoutor pool settings). - PHP script limit (
max_execution_time). - Database timeouts (MySQL wait timeout, lock wait timeout).
- External API timeouts (payment gateway, SMTP, license servers).
So when you see the error, don’t assume “raise max_execution_time and done.” Sometimes PHP is just the messenger. The real culprit is a slow database query, locked tables, overloaded CPU, saturated disk I/O, or a plugin doing something questionable like resizing 200 images in a single web request.
Opinionated take: treat max execution time errors like smoke alarms. You can remove the batteries, sure. But maybe check why the kitchen is on fire.
One quote worth keeping nearby
“Hope is not a strategy.” — Gene Kranz
Yes, it’s popular in ops circles. It’s also accurate. Guessing at timeouts is hope. Measuring is strategy.
Why it happens: the real bottlenecks
1) CPU starvation: PHP is waiting for its turn
PHP isn’t inherently slow, but it’s sensitive to contention. If your server has too many PHP-FPM workers, each gets a sliver of CPU. A task that “normally takes 2 seconds” now takes 45. Boom: timeout.
This is common on shared hosting, under-provisioned VPS, and “we moved to a smaller instance to save money” setups.
2) Disk I/O: the quiet killer
WordPress is chatty: reading PHP files, loading plugins, writing sessions, updating options, generating thumbnails. If storage is slow (network-attached volumes, oversubscribed disks, degraded RAID, bursting credits depleted), PHP can spend most of its “execution time” blocked on I/O.
3) Database bottlenecks: slow queries, locks, and vacuuming reality
When WordPress hangs, it’s often waiting on MySQL/MariaDB. Classic causes:
- Missing indexes introduced by plugin tables.
- Huge
wp_optionswith bloated autoloaded rows. - Lock contention during imports, batch updates, or misbehaving cron tasks.
- Long-running ALTER TABLE during business hours. Bold move.
4) External calls: your site is only as fast as that one third-party API
Payment gateways, shipping calculators, license checks, image optimization services, email providers—if a plugin makes synchronous HTTP calls during page load or admin actions, it inherits all the fragility of the internet.
5) WP-Cron: “not really cron” means “sometimes a pileup”
WP-Cron runs on page loads unless you disable it and use real cron. If traffic is low, jobs run late and then pile up. If traffic is high, jobs can run too often, overlap, and compete for resources. Either way, long cron jobs executed through HTTP requests are prime timeout candidates.
6) PHP memory pressure masquerading as timeouts
Sometimes the request is slow because it’s thrashing: allocating memory, hitting swap, triggering GC, or repeatedly failing to allocate enough memory for image processing. It might still surface as an execution timeout even though the underlying issue is memory.
Short joke #1: Timeouts are like deadlines—everyone ignores them until the boss enforces them.
Fast diagnosis playbook (check first/second/third)
If you want to find the bottleneck quickly, do it in this order. It minimizes guessing and maximizes “proof.”
First: identify which timeout fired (PHP vs PHP-FPM vs web server)
- If you see “Fatal error: Maximum execution time of X seconds exceeded” in logs or output, PHP’s
max_execution_timelikely triggered. - If you see 502/504 at the proxy, suspect Nginx/Apache/proxy timeouts or PHP-FPM termination.
- If you see PHP-FPM log lines like “request terminated”, suspect
request_terminate_timeoutor pool-level kill.
Second: decide whether it’s systemic (resource) or specific (one endpoint/job)
- Systemic: CPU pegged, load average high, disk latency high, many slow requests.
- Specific: always happens on plugin update, import, a particular admin page, or checkout.
Third: prove the wait (CPU vs disk vs DB vs network)
- CPU: high run queue, many PHP workers, long wall time with actual CPU time small.
- Disk: high iowait, slow
awaitiniostat. - DB: slow queries, lock waits, threads stuck in
Sending dataorWaiting for table metadata lock. - Network/external: requests stuck in HTTP calls; plugin logs show remote timeouts.
Fourth: choose the smallest safe fix
Most of the time, the smallest safe fix is not “set everything to 600 seconds.” It’s one of:
- Move the heavy work off the web request (WP-CLI, background jobs).
- Fix the slow query / add index / reduce autoload bloat.
- Right-size PHP-FPM workers to the CPU and memory you actually have.
- Fix disk I/O or move to better storage.
- Add sane timeouts and retries for external calls, with fallbacks.
Practical tasks: commands, outputs, and decisions (12+)
These are the “do it now, learn something” tasks. Each includes a command, example output, what it means, and the decision you make. Assumes Linux with systemd, Nginx or Apache, and PHP-FPM. Adjust paths to your distro.
Task 1: Confirm PHP max_execution_time (CLI baseline)
cr0x@server:~$ php -i | grep -E '^max_execution_time'
max_execution_time => 30 => 30
What it means: CLI PHP is set to 30 seconds. Web SAPI may differ, but this gives you a baseline.
Decision: If CLI shows a sane value (30–60) but web requests still timeout differently, focus on PHP-FPM and web server configs rather than global php.ini guessing.
Task 2: Confirm PHP-FPM pool effective settings
cr0x@server:~$ php-fpm8.2 -tt 2>/dev/null | head -n 20
[27-Dec-2025 10:11:02] NOTICE: configuration file /etc/php/8.2/fpm/php-fpm.conf test is successful
[27-Dec-2025 10:11:02] NOTICE: fpm is running, pid 1162
[27-Dec-2025 10:11:02] NOTICE: ready to handle connections
What it means: Config parses. Not proof of runtime values, but it prevents “you edited the wrong file” surprises.
Decision: If config test fails, stop and fix syntax. Timeouts caused by “service didn’t reload” are embarrassingly common.
Task 3: Check Nginx FastCGI timeouts
cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -E 'fastcgi_read_timeout|fastcgi_send_timeout|proxy_read_timeout' | head
fastcgi_read_timeout 60s;
fastcgi_send_timeout 60s;
What it means: Nginx will wait 60 seconds for PHP-FPM response. If PHP is allowed 120 seconds but Nginx waits 60, you’ll see 504s before PHP times out.
Decision: Align timeouts. Your edge/proxy timeout should generally be slightly higher than PHP’s for user-facing requests, unless you intentionally cap response time.
Task 4: Check Apache timeout (if using prefork/event with proxy_fcgi)
cr0x@server:~$ apachectl -S 2>/dev/null | head -n 5
VirtualHost configuration:
*:80 is a NameVirtualHost
default server example.com (/etc/apache2/sites-enabled/000-default.conf:1)
cr0x@server:~$ sudo apachectl -t -D DUMP_RUN_CFG 2>/dev/null | grep -E '^Timeout'
Timeout: 60
What it means: Apache waits 60 seconds. Again: if PHP thinks it can run 120 seconds, the client may never see it.
Decision: Either lower PHP to match and force background processing, or raise web timeout only for specific endpoints (imports) and keep the rest strict.
Task 5: Find the exact error in PHP-FPM and PHP logs
cr0x@server:~$ sudo grep -R "Maximum execution time" -n /var/log/php* /var/log/php8.2-fpm.log 2>/dev/null | tail -n 5
/var/log/php8.2-fpm.log:21944:PHP Fatal error: Maximum execution time of 30 seconds exceeded in /var/www/html/wp-includes/http.php on line 320
What it means: The stack trace points to WordPress HTTP requests. That’s usually a plugin/theme doing an external call, or WordPress core talking to itself.
Decision: Next step is to identify which plugin triggered the call (enable WP debug logging briefly, or reproduce with plugin disabled).
Task 6: Observe live resource pressure (CPU, memory, iowait)
cr0x@server:~$ top -b -n 1 | head -n 12
top - 10:13:44 up 12 days, 2:17, 1 user, load average: 7.12, 6.95, 6.20
Tasks: 245 total, 2 running, 243 sleeping, 0 stopped, 0 zombie
%Cpu(s): 22.1 us, 3.2 sy, 0.0 ni, 63.4 id, 11.0 wa, 0.0 hi, 0.3 si, 0.0 st
MiB Mem : 7842.1 total, 412.6 free, 6120.3 used, 1309.2 buff/cache
MiB Swap: 2048.0 total, 1880.0 free, 168.0 used. 1200.0 avail Mem
What it means: wa at 11% suggests disk I/O wait. Load average is high relative to CPU cores (interpret accordingly), meaning queues are forming.
Decision: Don’t raise timeouts yet. Investigate storage latency and PHP-FPM concurrency (too many workers can amplify I/O contention).
Task 7: Measure disk latency with iostat
cr0x@server:~$ iostat -xz 1 3
Linux 6.1.0 (server) 12/27/2025 _x86_64_ (4 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
18.52 0.00 3.01 12.44 0.00 66.03
Device r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await svctm %util
nvme0n1 82.00 140.00 3200.0 8200.0 78.00 2.10 14.80 0.55 12.30
What it means: await near ~15ms is not catastrophic, but for busy WordPress it can stack up. If you see 50–200ms, that’s a red alarm.
Decision: If await is consistently high, fix storage (move to faster volume, stop noisy neighbors, investigate RAID/degraded disk) before touching PHP timeouts.
Task 8: Check PHP-FPM pool saturation
cr0x@server:~$ sudo ss -s
Total: 1123 (kernel 0)
TCP: 317 (estab 54, closed 233, orphaned 0, timewait 233)
Transport Total IP IPv6
RAW 0 0 0
UDP 6 4 2
TCP 84 47 37
INET 90 51 39
FRAG 0 0 0
cr0x@server:~$ sudo ps -o pid,pcpu,pmem,etime,cmd -C php-fpm8.2 --sort=-pcpu | head
PID %CPU %MEM ELAPSED CMD
1422 18.2 2.7 01:03 php-fpm: pool www
1418 15.9 2.6 01:01 php-fpm: pool www
1399 12.4 2.5 00:59 php-fpm: pool www
What it means: Multiple workers burning CPU for over a minute suggests slow requests or deadlocks. If they’re all stuck at similar elapsed times, you may have a herd.
Decision: Check PHP-FPM status page (if enabled) or logs for “server reached pm.max_children.” If saturated, reduce concurrency or add CPU/memory; don’t just raise timeouts.
Task 9: Inspect MySQL for slow queries and lock waits
cr0x@server:~$ mysql -e "SHOW PROCESSLIST\G" | head -n 30
*************************** 1. row ***************************
Id: 27
User: wpuser
Host: localhost
db: wordpress
Command: Query
Time: 41
State: Sending data
Info: SELECT option_name, option_value FROM wp_options WHERE autoload='yes'
*************************** 2. row ***************************
Id: 29
User: wpuser
Host: localhost
db: wordpress
Command: Query
Time: 39
State: Waiting for table metadata lock
Info: ALTER TABLE wp_posts ADD COLUMN foo int(11)
What it means: One query is slow (wp_options autoload). Another session is blocked on a metadata lock due to schema change.
Decision: Stop doing schema changes in the middle of active traffic. Also audit wp_options autoload bloat—this is a classic WordPress self-own.
Task 10: Quantify autoloaded options size
cr0x@server:~$ mysql -N -e "SELECT ROUND(SUM(LENGTH(option_value))/1024/1024,2) AS autoload_mb FROM wp_options WHERE autoload='yes';"
12.47
What it means: 12.47MB of autoloaded options is large. WordPress loads these on every request. Congratulations: you’ve built a tiny config database inside your config database.
Decision: Identify the largest offenders and change autoload to ‘no’ when safe, or fix the plugin. If you’re above ~1–2MB, you’re usually paying a tax on every page view.
Task 11: Find the largest autoload offenders
cr0x@server:~$ mysql -e "SELECT option_name, ROUND(LENGTH(option_value)/1024,1) AS kb FROM wp_options WHERE autoload='yes' ORDER BY LENGTH(option_value) DESC LIMIT 10;"
+------------------------------+-------+
| option_name | kb |
+------------------------------+-------+
| some_plugin_cache_blob | 2048.3|
| another_plugin_settings | 512.8 |
| rewrite_rules | 256.4 |
| widget_custom_html | 188.1 |
+------------------------------+-------+
What it means: A plugin is storing a cache blob in autoload (bad), and other options are chunky.
Decision: For cache blobs: move to object cache or transients with expiration; set autoload ‘no’ if the plugin allows it. Don’t hand-edit without backups and a rollback plan.
Task 12: Check WP-Cron health and backlog with WP-CLI
cr0x@server:~$ cd /var/www/html && sudo -u www-data wp cron event list --fields=hook,next_run,recurrence --format=table | head
+------------------------------+---------------------+------------+
| hook | next_run | recurrence |
+------------------------------+---------------------+------------+
| wp_version_check | 2025-12-27 10:30:00 | twice_daily|
| woocommerce_cleanup_sessions | 2025-12-27 10:15:00 | hourly |
| some_plugin_heavy_job | 2025-12-27 10:01:00 | every_min |
+------------------------------+---------------------+------------+
What it means: A job running “every_min” is suspicious in WordPress land. It might overlap if it runs longer than a minute.
Decision: If a hook is too frequent or heavy, move it to real cron, reduce frequency, or refactor to batch.
Task 13: Reproduce the slow endpoint with curl and timing
cr0x@server:~$ curl -s -o /dev/null -w "code=%{http_code} t_total=%{time_total} t_connect=%{time_connect} t_starttransfer=%{time_starttransfer}\n" https://example.com/wp-admin/admin-ajax.php?action=some_action
code=504 t_total=60.012 t_connect=0.012 t_starttransfer=60.001
What it means: A hard 60-second cutoff lines up with Nginx fastcgi_read_timeout or proxy timeout. The backend likely kept running or got killed.
Decision: Check Nginx/Apache error logs around that time, then PHP-FPM logs for termination, then application logs for what it was doing.
Task 14: Inspect Nginx error log for upstream timeouts
cr0x@server:~$ sudo tail -n 20 /var/log/nginx/error.log
2025/12/27 10:16:02 [error] 2281#2281: *991 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 203.0.113.10, server: example.com, request: "POST /wp-admin/admin-ajax.php HTTP/2.0", upstream: "fastcgi://unix:/run/php/php8.2-fpm.sock", host: "example.com"
What it means: Nginx gave up waiting for PHP-FPM. Not necessarily that PHP died—just that the proxy stopped waiting.
Decision: If this endpoint is legitimately long-running, redesign it (async/background). If not, find the bottleneck inside PHP/DB.
Task 15: Confirm PHP-FPM termination settings
cr0x@server:~$ sudo grep -R "request_terminate_timeout" -n /etc/php/8.2/fpm/pool.d /etc/php/8.2/fpm/php-fpm.conf
/etc/php/8.2/fpm/pool.d/www.conf:392:request_terminate_timeout = 60s
What it means: PHP-FPM will kill requests at 60s even if PHP’s max_execution_time is higher.
Decision: Align. If you keep 60s at FPM, keep PHP lower or equal. Prefer lowering and moving long tasks out of band.
Task 16: Check if opcode cache is enabled (performance sanity)
cr0x@server:~$ php -i | grep -E '^opcache.enable|^opcache.memory_consumption'
opcache.enable => On => On
opcache.memory_consumption => 128 => 128
What it means: Opcache is on (good). If it were off, PHP would recompile scripts constantly, increasing latency and timeouts under load.
Decision: If opcache is off in FPM, turn it on. It’s one of the few “performance tweaks” that is actually boring and correct.
Safe fixes that don’t create new fires
There are two categories of fixes:
- Raise limits (timeouts, memory) to stop immediate errors.
- Make work faster or smaller so it fits within sane limits.
Raising limits is sometimes necessary, especially for imports or backups. But for routine page loads and admin clicks, it’s often a band-aid. And band-aids belong on cuts, not on arterial bleeding.
Fix 1: Increase max execution time (only with intent)
If you truly have a legitimate long-running request (large import, heavy report generation), you can raise max_execution_time. Do it in the right place:
- PHP-FPM: edit
/etc/php/8.2/fpm/php.inior pool overrides. - Apache mod_php: edit
/etc/php/8.2/apache2/php.ini. - CLI WP-CLI: edit
/etc/php/8.2/cli/php.ini(different from web).
Then reload services. Don’t restart the whole server unless you enjoy rituals.
Fix 2: Align the stack timeouts (proxy/web/FPM/PHP)
Misaligned timeouts create phantom failures: the client sees 504 while PHP continues, or PHP gets killed while Nginx is still patiently waiting.
A practical alignment for typical WordPress:
- User-facing requests: cap at 30–60 seconds (often less).
- Admin actions: 60–120 seconds if you must, but try not to.
- Imports/exports/backups: don’t do them via web request; use WP-CLI or background jobs.
When you do raise timeouts, do it narrowly. Use location blocks for import endpoints if needed. Don’t globally increase timeouts for everything and then wonder why your worker pool is full of zombies.
Fix 3: Move heavy admin work to WP-CLI
Web requests are a terrible place to do heavy lifting. They are user-driven, fragile, and timeout-limited. WP-CLI gives you long-running operations without proxy timeouts, and it’s easier to log.
Examples: plugin updates, database search/replace, large imports, cache rebuilds.
Fix 4: Replace “WP-Cron via traffic” with real cron
This is one of the best reliability upgrades you can make.
- Disable WP-Cron triggering on every request.
- Run
wp cron event run --due-nowfrom system cron every minute or five minutes.
It stabilizes job execution timing and reduces surprise spikes during page loads.
Fix 5: Fix autoload bloat
Autoload bloat is self-inflicted latency. The safe approach:
- Measure total autoload size.
- Identify top offenders.
- For each offender: determine if it’s safe to set autoload to ‘no’ or move it to transient/object cache.
- Test in staging. Deploy with rollback.
Fix 6: Fix slow queries and add indexes (carefully)
Indexes are not magic; they are trade-offs. But missing indexes on plugin tables is a recurring cause of timeouts.
Do not add indexes blindly in production during peak traffic. Adding an index can lock tables or at least increase I/O while building. Plan it.
Fix 7: Right-size PHP-FPM workers (pm settings)
More workers is not always more throughput. Too many workers can:
- blow out memory and force swap (slow, then slower),
- increase DB connections and contention,
- increase disk I/O contention.
Start with reality: CPU cores, memory, average request cost. Then set pm.max_children to a number you can actually run without swapping.
Fix 8: Address storage and filesystem realities
As the storage engineer in the room, here’s the part people skip because it isn’t a WordPress setting:
- If disk latency is high, optimize your storage. No amount of PHP tuning fixes slow disks.
- Check for filesystem full conditions: when disks fill up, everything gets weird.
- Network volumes (some “cheap” cloud disks) can have unpredictable latency under burst limits.
Short joke #2: Setting max_execution_time to 0 is like removing seatbelts because they wrinkle your shirt.
Three corporate mini-stories from the trenches
Mini-story #1: An incident caused by a wrong assumption
The company had a “simple” WooCommerce store. Not huge, not tiny. Enough revenue that downtime generated Slack messages with a certain tone. The ops team got a burst of checkout failures: customers reported spinning checkout and then errors. Nginx logs showed 504s.
The developer on call assumed it was a PHP timeout and bumped max_execution_time from 30 to 120. The errors got worse. That’s not unusual: longer timeouts mean more requests pile up. Concurrency rises, DB pressure rises, and everything slows further. The system became a slow-motion traffic jam.
The wrong assumption was that the “timeout error” meant “PHP needs more time.” What it actually meant was “PHP is stuck waiting.” MySQL processlist revealed multiple sessions blocked on a metadata lock. A plugin update had triggered an ALTER TABLE during business hours. The ALTER held locks long enough that checkout queries queued behind it, and PHP workers waited until they hit proxy timeouts.
The fix wasn’t “more time.” It was: stop the schema change, drain the queue, and schedule schema changes off-hours with a safer migration approach. Afterward, they implemented a rule: plugin updates that touch schema happen in a maintenance window, validated in staging against a copy of production data.
The postmortem was blunt: timeouts were the symptom. Locking was the disease. The team updated their runbook to check database locks before touching timeouts.
Mini-story #2: An optimization that backfired
A different org had a content-heavy WordPress site with aggressive caching. They wanted faster admin performance, so they tuned PHP-FPM for “maximum throughput”: increased pm.max_children substantially and reduced request timeouts “to fail fast.” The benchmark on a quiet staging box looked great.
Production disagreed. At peak editorial hours, many admins were uploading images and editing posts while traffic flowed. Disk I/O spiked because image processing and cache writes are write-heavy. With too many PHP-FPM workers, the disk queue length went up, I/O wait climbed, and each request slowed down. Requests began timing out—not because any individual request was “heavy,” but because the system was oversubscribed.
The optimization backfired because it assumed the bottleneck was CPU. It wasn’t. Storage was the bottleneck, and multiplying workers multiplied contention. The team had effectively built a concurrency amplifier on top of a slow disk.
The fix was counterintuitive: reduce PHP-FPM concurrency to match I/O capacity, enable opcache properly, and move image processing to an async queue. They also added disk latency monitoring as a first-class SLO signal instead of only CPU and memory.
After that, timeouts mostly disappeared. Not because they allowed more time. Because they stopped starting more work than the storage could finish.
Mini-story #3: A boring but correct practice that saved the day
A SaaS marketing team ran WordPress as their front door. They weren’t “technical,” but they had one SRE who insisted on boring habits: log retention, dashboards, a staging environment that mirrored production, and a monthly “update day” with rollback rehearsals.
One Tuesday, a plugin update introduced a regression: a new admin dashboard widget made a slow external API call on every admin page load. Editors started seeing max execution time errors. This could have turned into a full-day firefight.
But their boring practice kicked in. The SRE compared before/after graphs: outbound HTTP latency spiked at the exact moment of the plugin update. PHP logs showed the timeout originating in WordPress HTTP functions. They rolled back the plugin via their documented procedure (a real one, not “hope we have a backup”). Errors dropped immediately.
Then they redeployed with a mitigation: disabled the dashboard widget, added a short external timeout and caching, and pushed the vendor for a fix. The story ends with a line every ops person wants: “Customers barely noticed.”
The boring practice wasn’t magic. It was having observability and rollback before you need it.
Common mistakes: symptom → root cause → fix
These are the patterns I see repeatedly. If you recognize one, skip the heroic debugging and go straight to the likely cause.
1) Symptom: 504 Gateway Timeout in browser, no PHP fatal shown
Root cause: Nginx/Apache/proxy timeout is lower than PHP/PHP-FPM or the backend is stuck and proxy gives up first.
Fix: Check Nginx error log for “upstream timed out.” Align fastcgi_read_timeout with PHP-FPM termination settings. Then find why the backend is slow (DB locks, disk, CPU) instead of just raising the proxy timeout.
2) Symptom: “Maximum execution time of 30 seconds exceeded” during plugin updates
Root cause: Plugin updater doing filesystem writes slowly (permissions, slow disk), remote calls, or decompression under load.
Fix: Run updates via WP-CLI during low traffic. Ensure filesystem ownership is correct to avoid permission retries. Verify storage latency.
3) Symptom: Admin area slow, front end “mostly fine”
Root cause: Admin pages trigger more queries, load more plugins, call external APIs, or generate reports. Also more likely to hit autoload bloat and heavy hooks.
Fix: Profile admin endpoints; audit plugins that add dashboard widgets; reduce autoloaded options; disable unnecessary admin features in production.
4) Symptom: Errors cluster during imports or media uploads
Root cause: Image processing (GD/Imagick) is CPU+I/O heavy; imports do large DB inserts and can trigger locks.
Fix: Use WP-CLI for imports; batch operations; offload media processing or generate thumbnails async. Increase memory rather than time if you’re OOM-thrashing.
5) Symptom: Random timeouts, especially at low traffic
Root cause: WP-Cron pileup: tasks don’t run regularly, then a single request triggers multiple overdue tasks.
Fix: Disable WP-Cron and run real cron. Check for overlapping jobs and reduce frequency.
6) Symptom: After “increasing performance,” timeouts got worse
Root cause: Increased PHP-FPM workers or cache writes amplified contention (DB, disk). Or an “optimization” removed caching for admin endpoints, increasing load.
Fix: Revert to prior known-good config. Right-size concurrency. Measure iowait and DB lock waits. Make one change at a time.
7) Symptom: Timeouts only on one specific page/action
Root cause: A single plugin endpoint doing expensive work (remote calls, large query, full table scan).
Fix: Disable the plugin to confirm; then fix or replace it. Add indexes if needed. Move the work async.
Checklists / step-by-step plan
Checklist A: Get the site stable in 15 minutes (triage)
- Confirm the failure mode: PHP fatal vs 504 vs 502. Use logs, not vibes.
- Check resource pressure:
topandiostat. If iowait is high, don’t touch timeouts yet. - Check DB locks:
SHOW PROCESSLIST. If you see metadata locks, stop schema changes and clear blockers. - Identify the endpoint: Which URL/action triggers it? Reproduce with
curl -wtiming. - Mitigate: disable the offending plugin or feature if it’s clearly the cause; or route heavy tasks to WP-CLI.
Checklist B: Make the fix durable (next day work)
- Align timeouts across layers: proxy/web/FPM/PHP. Document it.
- Fix autoload bloat: measure, identify top offenders, remediate with tests.
- Fix slow queries: enable slow query logging temporarily; add indexes where justified.
- Implement real cron: disable WP-Cron and schedule it properly.
- Right-size PHP-FPM: set sensible
pm.max_childrenbased on memory and CPU. - Establish a safe update pipeline: staging validation, maintenance windows, rollback procedure.
Checklist C: When raising timeouts is acceptable
- Large one-off imports you control (prefer WP-CLI anyway).
- Admin-only reports with limited concurrency, where you also cap access.
- Backups/exports run via CLI or async job, not web requests.
If you’re raising timeouts for routine page loads or checkout, you’re probably treating symptoms.
Interesting facts and context (history matters)
- PHP’s execution time limit is decades old and was designed to protect shared servers from runaway scripts.
- WordPress’s “cron” isn’t system cron; it’s a pseudo-cron triggered by web traffic, which is clever until it isn’t.
- The classic 30-second default is common because many web servers historically assumed “a human won’t wait longer,” and because it prevents slow-loris style resource pinning.
- Timeout stacks are layered by design: each layer protects itself independently. That’s why fixing one timeout often reveals the next.
- Autoloaded options are loaded on every request, and the table can become a performance sink as plugins stash large blobs there.
- Many WordPress admin actions are synchronous (update, install, import), which is operationally awkward on production traffic.
- PHP-FPM added pool-level controls (like termination timeouts) partly to prevent single pools from monopolizing a server.
- Database metadata locks became a more visible issue as sites grew and “just run ALTER TABLE” collided with always-on traffic expectations.
- Object caching evolved as a workaround for repeated option and query loading, but it can also mask underlying bloat if you don’t measure.
FAQ
1) Should I just increase max_execution_time to 300?
Only if you can explain what work needs 300 seconds and why it must happen in a web request. For normal page loads, treat that as a bug to fix, not a limit to raise.
2) Why does it happen only when updating plugins/themes?
Updates involve downloading, verifying, unpacking archives, and writing many files. On slow disks or constrained CPU, it can exceed 30–60 seconds. Run updates via WP-CLI during low traffic and confirm filesystem permissions are correct.
3) I increased max_execution_time but I still get 504 errors. Why?
Because your proxy/web server timeout (Nginx fastcgi_read_timeout, Apache Timeout, load balancer idle timeout) is likely lower than PHP’s. Align them, then fix the underlying slowness.
4) What’s the difference between max_execution_time and request_terminate_timeout?
max_execution_time is enforced by PHP. request_terminate_timeout is enforced by PHP-FPM and can kill a request regardless of PHP’s setting. If they disagree, the smaller one wins in practice.
5) Can a database problem cause a PHP execution time error?
Yes. PHP “execution time” includes time spent waiting on the database. A slow query or lock can burn the entire budget while PHP is effectively idle.
6) Does increasing PHP memory limit help with execution time errors?
Sometimes. If the request is slow because it’s memory-thrashing or swapping, more memory can reduce wall time. But if it’s slow because of DB locks or disk latency, memory won’t save you.
7) How do I know if WP-Cron is involved?
Look for timeouts on normal page loads that correlate with cron hooks running, overdue events, or frequent custom hooks. Use WP-CLI to list events and identify heavy jobs. If overdue events pile up, move to system cron.
8) Is it safe to disable a plugin to fix timeouts?
In an incident, yes—if you understand the business impact. Disable the plugin that triggers the timeout to restore service, then investigate. For WooCommerce/payment plugins, be careful: disabling can break checkout. Prefer feature flags or targeted disabling where possible.
9) Why does it happen on shared hosting more often?
Shared hosting tends to have stricter limits, noisy neighbors, slower storage, and less control over PHP-FPM and proxy settings. You’re also more likely to be stuck with low defaults and limited observability.
10) What’s the most “SRE-correct” fix for long tasks?
Make them asynchronous: enqueue work, return quickly, and process in the background with retries, timeouts, and idempotency. WordPress can do this imperfectly, but it’s still better than pinning web workers.
Conclusion: next steps you should actually do
If you remember one thing: execution time errors are usually the server telling you “this request is too big, or the system is too slow.” The safe path is to prove which one.
- Classify the timeout (PHP vs proxy vs FPM) using logs.
- Run the fast diagnosis: CPU/iowait, DB locks, endpoint reproduction.
- Fix the bottleneck (autoload bloat, slow queries, storage latency, oversubscribed FPM) before raising limits.
- Move heavy work off web requests: WP-CLI, real cron, background jobs.
- Align timeouts intentionally, document them, and keep them tight for user-facing paths.
Do that, and “Max execution time exceeded” goes back to being what it should be: a guardrail you rarely hit, not a lifestyle.