...

Uneven CPU load in WordPress – how cron jobs can destroy performance

Uneven CPU load in WordPress is often caused by poorly configured WordPress cron jobs, that start as background processes every time a page is accessed, causing spikes. I'll show you how these triggers increase TTFB, tie up PHP workers, and create latency—and how you can get back to more evenly Last one.

Key points

The following overview summarizes the most important aspects before I go into more detail and explain specific steps. I will keep the list short so that the focus remains on Action and effect lies.

  • WP-Cron Triggers when pages are viewed and generates unpredictable load.
  • PHP processes accumulate during traffic and slow down TTFB.
  • System cron Separates tasks from visitor flow.
  • Intervals and priorities smooth out CPU spikes.
  • Monitoring Identifies bottlenecks and faulty events.

What WordPress cron jobs really do – and where the load comes from

WordPress relies on a pseudo-cron system: When called, wp-cron.php is triggered via POST, checks for due events, and starts tasks such as publications, update checks, draft saving via Heartbeat, and database cleanup—each event costs CPU time. This approach sounds convenient, but it causes uncontrollable triggers because visits determine execution rather than a predictable timer. When multiple calls coincide, parallel PHP processes start up and compete for workers. Multisite setups amplify the effect because each subsite maintains its own event stack, thus increasing the number of checks [1]. If you want to learn more about the interrelationships, you will find sound fundamentals at Understanding WP-Cron, but the core message remains: visitor management is not suitable as a reliable clock generator.

The real bottleneck: parallel PHP processes caused by wp-cron.php

Each cron trigger starts a separate PHP process that binds a worker, thereby reducing the available computing time for actual page renderings. If triggers accumulate, the waiting time for a free worker increases, TTFB lengthens, and the first byte arrives later at the browser [2]. Measurements showed a delay of up to 800 milliseconds, which impacts Core Web Vitals and dampens organic visibility [3]. Shared hosting or tight PHP-FPM settings exacerbate the effect because max_children is quickly reached and processes end up in queues. Especially during shop peaks or campaigns, this can become a vicious circle: more traffic generates more cron checks, which in turn block rendering and thus Loading times stretch [1][2].

Handling caching, CDN, and loopback traps correctly

By default, WP-Cron uses an internal loopback request to your own domain. If there is an aggressive page cache, a CDN, or a basic auth lock in front of it, the call may fail or wait—cron jobs stall, repeat, and thus extend CPU binding. I therefore ensure that /wp-cron.php not cached, not rate-limited, and internally accessible. System cron mitigates this vulnerability because it without HTTP loopback directly executes PHP. If a proxy is upstream, I also check whether requests to 127.0.0.1 be passed on cleanly and no WAF rule blocks the endpoint. During maintenance phases, it is important to either deliberately pause Cron or explicitly allow the endpoint to pass through so that due tasks are not „re-fired“ as a package.

Detecting and classifying uneven CPU load

Peak loads during rush hours are typical and cannot be explained by visitor numbers alone, but rather by cron waves from overdue events that pile up and fire together. Multisite installations multiply the load, as each subsite manages cron lists and is checked during visits – this results in short but intense peaks, which log files show as cascades of wp-cron.php POSTs [1]. Plugins often register their own events at intervals that are too short, sometimes every five minutes or more, which quickly adds up to dozens of checks per call with ten plugins. Also pay attention to your PHP worker limit, because full workers cause delays that users directly experience. Those who read these patterns understand the uneven curve as a result of triggers, not as inevitable. mood of the traffic.

Why System Cron Smoothes the Load

A genuine system cron decouples tasks from visitor traffic and sets a clear cycle, for example every five minutes, hourly, or daily—this makes execution predictable and distributes the load evenly. Visitors no longer trigger cron jobs, which relieves TTFB and prioritizes rendering. Even with low traffic, tasks run reliably because the server executes them even when no one is visiting the site. This helps updates, emails, or index pings run on time and prevents events from „lingering“ and firing later as a package. This is how I create a predictable system load, that does not fluctuate according to traffic conditions.

Step by step: Disable WP-Cron and set up System Cron

I start by disabling the internal trigger in wp-config.php so that page views no longer trigger cron jobs. To do this, add the following line and save the file so that WordPress does not trigger a cron check when rendering. Then I set up a clean crontab rule that triggers wp-cron.php cyclically without generating unnecessary output. This way, the job runs on a schedule and consistently reduces the load on page views. The result: rendering takes priority, cron jobs have their own clocking.

// wp-config.php define('DISABLE_WP_CRON', true);
# Crontab example (every 5 minutes) */5 * * * * php -q /var/www/html/wp-cron.php > /dev/null 2>&1

WP-CLI instead of direct PHP calls

For better control, I like to set the cron run via WP-CLI This allows me to execute „only due“ events, log them in more detail, and process multisites in a targeted manner. In addition, a lock prevents multiple runs from starting in parallel.

# WP-CLI: only process due events */5 * * * * /usr/local/bin/wp cron event run --due-now --path=/var/www/html --quiet

# With simple lock via flock (recommended) */5 * * * * flock -n /tmp/wp-cron.lock /usr/local/bin/wp cron event run --due-now --path=/var/www/html --quiet

In multisite environments, I can use --url= Go through the sites one by one or use a small shell loop to rotate the sites. This prevents 100 subsites from hitting the same beat at the same time and creating load peaks.

Intervals and priorities: which tasks should run when

Not every task needs to be done every minute; I prioritize according to relevance and cost so that SEO-critical jobs take precedence and expensive work is moved to off-peak times [1]. The focus is on sitemap generation, indexing pings, and cache warming, followed by database maintenance and transient deletions. I schedule backups during nighttime windows and choose incremental methods to avoid I/O spikes. I consolidate newsletter queues or importers and run them in fixed slots instead of checking them every time a page is accessed. This order ensures clear priorities and prevents short poll intervals from CPU clog.

Task Recommended interval CPU impact Note
Sitemap/Indexing Pings hourly to once a day low SEO-relevant; before cache warming prioritize
cache warming 1–2 times per day medium Stagger URLs, no full scans during peak hours
Backups at night high Incremental; remote destination with bandwidth limit
Database cleanup daily or weekly medium Revisions/transients in blocks delete
email notifications hourly/once a day low Create batches, use queue

Single-run mechanisms and clean locks

To prevent cron jobs from overlapping, I set flock also WordPress's own restrictions. WP_CRON_LOCK_TIMEOUT defines how long a run remains exclusive. If the page is slow or long jobs are running, I increase the value moderately so that no second process starts prematurely. Conversely, I lower it if jobs are short and a hang-up should not trigger cascades.

// wp-config.php – Lock time in seconds (default 60) define('WP_CRON_LOCK_TIMEOUT', 120);

In addition, I deliberately limit parallelism in plugins (batch sizes, step lengths, sleeps between requests). This prevents a cron run from generating dozens of PHP processes itself and causing the load curve to spike.

Monitoring and analysis: Making bottlenecks visible

I start with the access logs and filter POST requests on wp-cron.php to identify frequency and time windows; many short intervals indicate tight intervals or blocking events. At the same time, I check error logs for timeouts, locking, and database wait times that affect cron jobs. In the backend, WP Crontrol provides insight into registered events, their hooks, and scheduled runtimes; there, I delete outdated or hanging entries. For deeper insight into transactions, query times, and PHP-FPM queues, I use APM tools for WordPress to isolate hotspots. This allows me to find the causes instead of just suppressing the symptoms, and I can take targeted action. Measures prioritize.

Measurable goals and a quick 10-minute test

I define clear target values: TTFB p95 for cached pages below 200–300 ms, for uncached pages below 800 ms; PHP-FPM queue permanently close to 0; CPU without sharp peaks that run into saturation. The quick test: Deactivate WP-Cron, set system cron, process due events once via WP-CLI, then check logs. In 10 minutes, you'll see if the TTFB drops, the PHP queue shrinks, and if conspicuous hooks (e.g., update checks, importers) are responsible for the majority of the load. Then adjust intervals, batch sizes, and the clock speed until the curves are stable.

Taming Heartbeat API and Plugin Events

The heartbeat mechanism updates sessions and drafts, but often generates unnecessary requests in the frontend; I throttle it to admin areas or set appropriate intervals. Many plugins register cron jobs with factory settings that are too frequent; here, I switch to longer intervals and move tasks to off-peak times. In shop setups, I limit inventory feeds and price syncs to fixed slots instead of polling every minute. For feeds and cache warming, I use batch lists so that not all URLs run in one go. These interventions reduce request frequencies and smooth out the curve clearly.

Scaling: From cron jobs to queues and workers

When traffic is high, I keep WP-Cron as small as possible and move computationally intensive tasks to queues with dedicated workers. Job queues distribute load across multiple processes, can be expanded horizontally, and prevent the frontend from having to wait. In container or orchestration setups, I scale workers independently of PHP-FPM, giving rendering and background work separate resources. Queues are particularly useful for imports, image processing, newsletter batches, and API syncs. This keeps the frontend responsive while background jobs are controlled and plannable run.

WooCommerce, Action Scheduler, and large queues

WooCommerce brings with the Action Scheduler I have my own queue that processes order emails, webhooks, subscriptions, and syncs. This is where CPU spikes are most likely to occur when thousands of actions are „due.“ I don't run the runner when the page is called up, but trigger it via system cron or WP-CLI in fixed windows. I set batch sizes and parallelism so that a run does not block the database and PHP-FPM workers remain free. I distribute importers, image regeneration, and webhook peaks across several small runs—better 10× short than 1× for hours with I/O blockages.

Multisite specifics: Balancing clocking per site

In multisite setups, the load adds up because each site has its own event list. Instead of checking everything every five minutes, I rotate the sites: site groups with slightly offset cycles so that not all cron lists run at the same time. For highly active sites, the queue gets a slot more often, while quiet sites get one less often. The result is a more even CPU curve and less competition for workers – with the same total workload.

Practical check: Configuration without pitfalls

First, I check whether DISABLE_WP_CRON is set correctly, because duplicate triggers (internal + external) exacerbate load peaks. Then I check the crontab: correct path, no unnecessary output, reasonable interval, and no overlaps with backup windows. In WP Crontrol, I clean up outdated hooks and change short intervals to realistic cycles. I adjust the PHP-FPM settings to the visitor profile so that PHP workers don't constantly hit the upper limit. Finally, I track TTFB, response times, and CPU to clearly see the effect of the changes. Rate.

Properly dimension PHP-FPM, OPCache, and time limits

The best cron strategy is useless if PHP-FPM is too small or incorrectly timed. I choose pm=dynamic or pm=on demand depending on the traffic profile and forward pm.max_children from the actual RAM budget: As a rule of thumb, RAM_for_PHP / average script consumption. Example: 2 GB budget and ~128 MB per process result in ~16 workers. pm.max_requests I set it moderately (500–1000) to cap leaks. request_terminate_timeout Limited outlier jobs; a clean slowlog Detects query loops and external wait times. A healthy OPCache (sufficient max_accelerated_files, memory_consumption, interned_strings_buffer) prevents cold starts and saves CPU per request – even for cron jobs.

Object cache and options hygiene

A persistent object cache (e.g., Redis/Memcached) significantly reduces database pressure for cron checks. It is important to note that hygiene in the wp_optionsTable: The option cron must not grow to several megabytes, otherwise each check becomes expensive. I remove outdated or hanging events, reduce autoload junk, and set large, rarely used options to autoload = no. This reduces query times and allows cron lists to be evaluated more quickly.

Fine-tuning: timing, sequence, and resource limits

For websites with peaks in the morning, I schedule cache warming for the early evening and run sitemaps shortly before business hours so that crawlers see fresh data. I split expensive database cleanups into smaller blocks to reduce lock times and prevent query spikes. I schedule large exports for weekend windows when there is less interaction. Where appropriate, I limit parallel jobs so that multiple cron-php processes do not compete for I/O at the same time. This fine-tuning ensures consistent throughput and better Response times.

Security: Protect wp-cron.php from external access

Because Cron is to be triggered internally, I block direct external access to /wp-cron.php. This prevents abuse, DDoS attacks, and accidental external calls. Only allow local calls (loopback or CLI) and block everything else. This reduces noise in the logs and protects PHP workers.

# Nginx example location = /wp-cron.php { allow 127.0.0.1; deny all; include fastcgi_params; fastcgi_pass php-fpm; }

# Apache example  Require ip 127.0.0.1

Common causes of „ghost“ load due to cron

Very short intervals (1–5 minutes) for non-critical tasks are among the biggest load drivers, especially in combination with many plugins. Hanging events, which are repeatedly rescheduled due to failed runs, create loops that flood logs. Locking problems in the database force cron jobs to run longer, increasing overlap. In addition, HTTP blockages (e.g., DNS or remote API) can artificially prolong cron runs and tie up workers. Knowing these patterns saves a lot of time when searching for causes and reduces the Peaks fast.

Briefly summarized

Uneven CPU load in WordPress often originates from WP-Cron, which acts as a trigger when pages are viewed and binds PHP workers. I disable the internal trigger, set up a system cron, optimize intervals, and prioritize SEO-relevant tasks so that rendering takes precedence. Monitoring with logs, WP Crontrol, and APM analyses shows me faulty events, tight cycles, and blocking processes. For large projects, I move computationally intensive work into queues to cleanly separate frontend and background jobs. This approach leads to even load, shorter TTFB, and noticeable faster Delivery – without unexpected peaks.

Current articles