Ungleichmäßige CPU-Last in WordPress – wie Cronjobs Performance zerstören können

Ungleichmäßige CPU-Last in WordPress entsteht oft durch schlecht konfigurierte wordpress cronjobs, die bei jedem Seitenaufruf als Hintergrundprozesse starten und so Spitzen verursachen. Ich zeige, wie diese Trigger die TTFB verlängern, PHP-Worker binden und Latenzen erzeugen – und wie du mit System-Cron, Intervallen und Priorisierung wieder zu gleichmäßiger Last kommst.

Zentrale Punkte

Die folgende Übersicht bringt die wichtigsten Aspekte auf den Punkt, bevor ich tiefer einsteige und konkrete Schritte erkläre. Ich halte die Liste kurz, damit der Fokus auf Handlung und Wirkung liegt.

  • WP-Cron triggert bei Seitenaufrufen und erzeugt unplanbare Last.
  • PHP-Prozesse stauen sich bei Traffic und verlangsamen TTFB.
  • System-Cron entkoppelt Aufgaben vom Besucherstrom.
  • Intervalle und Prioritäten glätten CPU-Spitzen.
  • Monitoring deckt Engpässe und fehlerhafte Events auf.

Was WordPress-Cronjobs wirklich tun – und wo die Last herkommt

WordPress setzt auf ein Pseudo-Cron-System: Beim Aufruf wird wp-cron.php per POST ausgelöst, prüft fällige Events und startet Tasks wie Veröffentlichungen, Update-Checks, Entwurfs-Speichern via Heartbeat und Datenbank-Aufräumarbeiten – jedes Ereignis kostet CPU-Zeit. Dieser Ansatz klingt komfortabel, verursacht aber unkontrollierbare Trigger, weil Besuche die Ausführung bestimmen und nicht ein planbarer Zeitgeber. Treffen mehrere Aufrufe zusammen, starten parallele PHP-Prozesse, die um Worker konkurrieren. Multisite-Setups verstärken den Effekt, da jede Subsite ihren eigenen Event-Stack pflegt und so die Anzahl der Prüfungen erhöht [1]. Wer die Zusammenhänge vertiefen will, findet fundierte Grundlagen unter WP-Cron verstehen, doch die Kernbotschaft bleibt: Besucherlenkung eignet sich nicht als verlässlicher Taktgeber.

Die eigentliche Bremse: parallele PHP-Prozesse durch wp-cron.php

Jeder Cron-Trigger startet einen separaten PHP-Prozess, der einen Worker bindet und dadurch die verfügbare Rechenzeit für echte Seitenrenderings reduziert. Häufen sich Trigger, steigt die Wartezeit auf einen freien Worker, TTFB verlängert sich, und der erste Byte kommt später beim Browser an [2]. In Messungen zeigte sich eine Verzögerung um bis zu 800 Millisekunden, was Core Web Vitals belastet und die organische Sichtbarkeit dämpft [3]. Shared-Hosting oder knapp bemessene PHP-FPM-Settings verschärfen den Effekt, weil max_children schnell erreicht werden und Prozesse in Warteschlangen landen. Gerade bei Shop-Peaks oder Kampagnen kann das zu einem Teufelskreis werden: Mehr Traffic erzeugt mehr Cronprüfungen, die wiederum Rendering blockieren und so Ladezeiten strecken [1][2].

Caching, CDN und Loopback-Fallen richtig behandeln

WP-Cron nutzt standardmäßig einen internen Loopback-Request auf die eigene Domain. Liegen davor ein aggressiver Page-Cache, ein CDN oder eine Basic-Auth-Sperre, kann der Aufruf fehlschlagen oder warten – Cronläufe stocken, wiederholen sich und verlängern so die CPU-Bindung. Ich sorge deshalb dafür, dass /wp-cron.php nicht gecacht, nicht rate-limited und intern erreichbar ist. System-Cron entschärft diese Schwachstelle, weil er ohne HTTP-Loopback direkt PHP ausführt. Wenn ein Proxy vorgeschaltet ist, prüfe ich zusätzlich, ob Requests an 127.0.0.1 sauber durchgereicht werden und keine WAF-Regel den Endpunkt blockiert. In Maintenance-Phasen ist wichtig: Entweder Cron bewusst pausieren – oder den Endpunkt explizit durchlassen, damit fällige Tasks nicht als Paket „nachfeuern“.

Ungleichmäßige CPU-Last erkennen und einordnen

Typisch sind Lastspitzen zu Stoßzeiten, die sich nicht allein durch Besucherzahlen erklären lassen, sondern durch Cron-Wellen aus überfälligen Events, die sich stapeln und gemeinsam feuern. Multisite-Installationen vervielfachen die Last, da jede Subsite Cronlisten verwaltet und beim Besuch geprüft wird – so entstehen kurze, aber harte Peaks, die Logfiles als Kaskaden von wp-cron.php-POSTs zeigen [1]. Häufig registrieren Plugins eigene Events mit zu kurzen Intervallen, teils alle fünf Minuten oder öfter, was sich bei zehn Plugins schnell zu Dutzenden Prüfungen je Aufruf summiert. Achte zusätzlich auf dein PHP-Worker Limit, denn volle Worker erzwingen Wartezeiten, die Nutzer direkt spüren. Wer diese Muster liest, versteht die ungleichmäßige Kurve als Folge von Triggern, nicht als unvermeidliche Laune des Traffics.

Warum System-Cron die Last glättet

Ein echter System-Cron entkoppelt Aufgaben vom Besucherstrom und setzt einen klaren Takt, etwa alle fünf Minuten, stündlich oder täglich – damit wird die Ausführung planbar und die Last gleichmäßig verteilt [1][6]. Besuchende lösen dann keine Cronjobs mehr aus, was TTFB entlastet und Rendering priorisiert. Auch bei wenig Traffic laufen Tasks zuverlässig, weil der Server sie ausführt, selbst wenn niemand die Seite besucht. Das hilft Updates, Mails oder Index-Pings, pünktlich zu laufen und verhindert, dass Events „liegen bleiben“ und später als Paket feuern. So schaffe ich eine vorhersehbare Systemlast, die nicht nach Laune des Traffics schwankt.

Schritt für Schritt: WP-Cron deaktivieren und System-Cron einrichten

Ich beginne mit dem Abschalten des internen Triggers in der wp-config.php, damit kein Seitenaufruf mehr Cronjobs startet. Füge dazu die folgende Zeile hinzu und speichere die Datei, damit WordPress keine Cron-Prüfung beim Rendern anstößt. Danach richte ich eine saubere Crontab-Regel ein, die wp-cron.php zyklisch anstößt, ohne unnötige Ausgabe zu erzeugen. So läuft der Job zeitgesteuert und entlastet Seitenaufrufe konsequent. Das Ergebnis: Rendering hat Vorrang, Cronjobs haben eine eigene Taktung.

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

WP-CLI statt direktem PHP-Aufruf

Für bessere Kontrolle setze ich den Cronlauf gern per WP-CLI ab. So kann ich „nur fällige“ Events ausführen, detaillierter loggen und Multisite gezielt abarbeiten. Zusätzlich verhindert ein Lock, dass mehrere Läufe parallel starten.

# WP-CLI: nur fällige Events abarbeiten
*/5 * * * * /usr/local/bin/wp cron event run --due-now --path=/var/www/html --quiet

# Mit einfachem Lock über flock (empfohlen)
*/5 * * * * flock -n /tmp/wp-cron.lock /usr/local/bin/wp cron event run --due-now --path=/var/www/html --quiet

In Multisite-Umgebungen kann ich so per --url= Site für Site durchgehen oder über ein kleines Shell-Loop die Sites rotieren lassen. Das vermeidet, dass 100 Subsites zeitgleich den gleichen Takt treffen und Lastspitzen erzeugen.

Intervalle und Prioritäten: welche Aufgaben wann laufen sollten

Nicht jede Aufgabe braucht Minutentakt; ich staffele nach Relevanz und Kosten, damit SEO-kritische Jobs Vorrang erhalten und teure Arbeiten in Nebenzeiten wandern [1]. Im Fokus stehen Sitemap-Erzeugung, Indexing-Pings und Cache-Warming, danach folgen Datenbankpflege und Transient-Löschungen. Backups plane ich in Nachtfenstern und wähle inkrementelle Verfahren, um I/O-Spitzen zu vermeiden. Newsletter-Queues oder Importer fasse ich zusammen und lasse sie in festen Slots laufen, statt sie bei jedem Seitenaufruf zu prüfen. Diese Ordnung sorgt für klare Prioritäten und verhindert, dass kurze Poll-Intervalle die CPU verstopfen.

Aufgabe Empfohlenes Intervall CPU-Impact Hinweis
Sitemap/Indexing-Pings stündlich bis 1×/Tag niedrig SEO-relevant; vor dem Cache-Warming priorisieren
Cache-Warming 1–2×/Tag mittel URLs staffeln, keine Voll-Scans zur Hauptzeit
Backups nachts hoch inkrementell; Remote-Ziel mit Bandbreitenlimit
Datenbankbereinigung täglich oder wöchentlich mittel Revisionen/Transients in Blöcken löschen
E-Mail-Benachrichtigungen stündlich/1×/Tag niedrig Batches bilden, Queue nutzen

Single-Run-Mechanismen und saubere Locks

Damit Cronläufe sich nicht überlappen, setze ich neben flock auch WordPress-eigene Schranken ein. WP_CRON_LOCK_TIMEOUT definiert, wie lange ein Lauf exklusiv bleibt. Ist die Seite langsam oder laufen lange Jobs, erhöhe ich den Wert moderat, damit kein zweiter Prozess frühzeitig startet. Umgekehrt senke ich ihn, wenn Jobs kurz sind und ein Hänger keine Kaskaden auslösen soll.

// wp-config.php – Lock-Zeit in Sekunden (Standard 60)
define('WP_CRON_LOCK_TIMEOUT', 120);

Zusätzlich begrenze ich in Plugins bewusst Parallelität (Batch-Größen, Schrittlängen, Sleeps zwischen Requests). So verhindere ich, dass ein Cronlauf selbst wieder Dutzende PHP-Prozesse erzeugt und die Lastkurve aufschaukelt.

Monitoring und Analyse: Engpässe sichtbar machen

Ich starte bei den Access-Logs und filtere POST-Requests auf wp-cron.php, um Häufigkeit und Zeitfenster zu erkennen; viele kurze Abstände deuten auf enge Intervalle oder blockierende Events hin. Parallel prüfe ich Fehler-Logs auf Timeouts, Locking und Datenbankwartezeiten, die Cronjobs beeinflussen. Im Backend verschafft WP Crontrol Einblick in registrierte Events, ihre Hooks und geplante Laufzeiten; dort lösche ich veraltete oder hängende Einträge. Für tieferen Einblick in Transaktionen, Query-Zeiten und PHP-FPM-Warteschlangen setze ich APM-Tools für WordPress ein, um Hotspots zu isolieren. So finde ich die Ursachen, statt nur Symptome zu dämpfen, und kann gezielt Maßnahmen priorisieren.

Messbare Ziele und Kurztest in 10 Minuten

Ich definiere klare Zielwerte: TTFB p95 für gecachte Seiten unter 200–300 ms, für uncached Seiten unter 800 ms; PHP-FPM-Queue dauerhaft nahe 0; CPU ohne spitze Peaks, die in Sättigung laufen. Der Kurztest: WP-Cron deaktivieren, System-Cron setzen, fällige Events einmalig per WP-CLI abarbeiten, dann Logs prüfen. In 10 Minuten siehst du, ob die TTFB fällt, die PHP-Queue schrumpft und ob auffällige Hooks (z. B. Update-Checks, Importer) den Hauptanteil tragen. Danach justiere Intervalle, Batch-Größen und den Takt, bis die Kurven stabil sind.

Heartbeat-API und Plugin-Events zähmen

Der Heartbeat-Mechanismus aktualisiert Sitzungen und Entwürfe, erzeugt aber im Frontend oft unnötige Requests; ich drossele ihn auf Admin-Bereiche oder setze passende Intervalle. Viele Plugins registrieren Cronjobs mit Werkswerten, die zu eng takten; hier stelle ich auf längere Abstände um und verschiebe Tasks in Nebenzeiten. In Shop-Setups begrenze ich Inventar-Feeds und Preis-Syncs auf feste Slots, statt minütlich zu pollen. Für Feeds und Cache-Warming nutze ich Batch-Listen, damit nicht alle URLs in einem Rutsch laufen. Diese Eingriffe senken Request-Frequenzen und glätten die Kurve deutlich.

Skalierung: Von Cronjobs zu Queues und Workern

Bei hohem Traffic halte ich WP-Cron möglichst klein und verlagere rechenintensive Tasks in Queues mit dedizierten Workern. Job-Queues verteilen Last auf mehrere Prozesse, lassen sich horizontal erweitern und vermeiden, dass das Frontend warten muss. In Container- oder Orchestrierungs-Setups skaliere ich Worker unabhängig von PHP-FPM, wodurch Rendern und Hintergrundarbeit getrennte Ressourcen bekommen. Für Importe, Bildverarbeitung, Newsletter-Batches und API-Syncs zahlen sich Queues besonders aus. So bleibt das Frontend reaktionsschnell, während Hintergrundjobs kontrolliert und planbar laufen.

WooCommerce, Action Scheduler und große Queues

WooCommerce bringt mit dem Action Scheduler eine eigene Queue mit, die Bestell-Mails, Webhooks, Abos und Syncs verarbeitet. Gerade hier drohen CPU-Spitzen, wenn tausende Aktionen „due“ sind. Ich lasse den Runner nicht beim Seitenaufruf laufen, sondern triggere ihn über System-Cron oder WP-CLI in festen Fenstern. Batch-Größen und Parallelität stelle ich so ein, dass ein Lauf die Datenbank nicht blockiert und PHP-FPM-Worker frei bleiben. Importer, Bildregeneration und Webhook-Spitzen verteile ich in mehrere kleine Durchläufe – lieber 10× kurz als 1× stundenlang mit I/O-Blockaden.

Multisite-Spezifika: Taktung per Site balancieren

In Multisite-Setups summiert sich die Last, weil jede Site ihre eigene Eventliste hat. Statt alles alle fünf Minuten zu prüfen, rotiere ich die Sites: Site-Gruppen mit leicht versetzten Takten, damit nicht alle Cronlisten gleichzeitig laufen. Für stark aktive Sites erhält die Queue öfter einen Slot, ruhige Sites seltener. Das Ergebnis ist eine gleichmäßigere CPU-Kurve und weniger Konkurrenz um Worker – bei gleicher Gesamtarbeit.

Praxis-Check: Konfiguration ohne Stolperfallen

Ich prüfe zuerst, ob DISABLE_WP_CRON korrekt gesetzt ist, denn doppelte Trigger (intern + extern) verschärfen Lastspitzen. Danach kontrolliere ich die Crontab: korrekter Pfad, kein unnötiger Output, sinnvoller Intervall, und keine Überschneidungen mit Backup-Fenstern. In WP Crontrol bereinige ich veraltete Hooks und stelle enge Intervalle auf realistische Zyklen um. Die PHP-FPM-Settings passe ich an das Besucherprofil an, damit PHP-Worker nicht ständig an der Obergrenze hängen. Abschließend tracke ich TTFB, Antwortzeiten und CPU, um den Effekt der Änderungen sauber zu bewerten.

PHP-FPM, OPCache und Zeitlimits passend dimensionieren

Die beste Cron-Strategie verpufft, wenn PHP-FPM zu klein oder falsch getaktet ist. Ich wähle pm=dynamic oder pm=ondemand je nach Traffic-Profil und leite pm.max_children aus dem realen RAM-Budget ab: Als Daumenregel RAM_für_PHP / durchschnittlicher Scriptverbrauch. Beispiel: 2 GB Budget und ~128 MB je Prozess ergeben ~16 Worker. pm.max_requests setze ich moderat (500–1000), um Leaks zu kappen. request_terminate_timeout begrenzt Ausreißer-Jobs; ein sauberer slowlog deckt Query-Schleifen und externe Wartezeiten auf. Ein gesunder OPCache (genügend max_accelerated_files, memory_consumption, interned_strings_buffer) verhindert kalte Starts und spart CPU pro Request – auch für Cronläufe.

Object Cache und Optionen-Hygiene

Ein persistenter Object Cache (z. B. Redis/Memcached) reduziert Datenbankdruck für Cronprüfungen deutlich. Wichtig ist dabei die Hygiene in der wp_options-Tabelle: Die Option cron darf nicht auf mehrere Megabyte anwachsen, sonst wird jede Prüfung teuer. Veraltete oder hängende Events entferne ich, autoload-Schrott reduziere ich, und große, selten genutzte Optionen stelle ich auf autoload = no. So sinken Query-Zeiten und Cronlisten lassen sich schneller evaluieren.

Feintuning: Takt, Reihenfolge und Ressourcengrenzen

Für Websites mit Spitzen am Vormittag takte ich Cache-Warming auf die frühe Nacht und lasse Sitemaps kurz vor Geschäftszeiten laufen, damit Crawler frische Daten sehen. Teure Datenbankbereinigungen splitte ich in kleinere Blöcke, um Lock-Zeiten zu senken und Query-Spitzen zu verhindern. Große Exporte setze ich auf Wochenendfenster, in denen weniger Interaktion stattfindet. Wo sinnvoll, begrenze ich parallele Jobs, damit nicht mehrere cron-php-Prozesse zeitgleich um I/O kämpfen. Dieses Feintuning sorgt für gleichmäßigen Durchsatz und bessere Antwortzeiten.

Sicherheit: wp-cron.php vor externen Zugriffen schützen

Weil Cron intern ausgelöst werden soll, sperre ich direkten externen Zugriff auf /wp-cron.php. So verhinderst du Missbrauch, DDoS und versehentliche Fremdaufrufe. Erlaube nur lokale Aufrufe (Loopback oder CLI) und blocke alles andere. Das senkt Rauschen in den Logs und schützt PHP-Worker.

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

# Apache-Beispiel
<Files "wp-cron.php">
  Require ip 127.0.0.1
</Files>

Häufige Ursachen für „Ghost“-Last durch Cron

Sehr kurze Intervalle (1–5 Minuten) für unkritische Tasks gehören zu den größten Lasttreibern, besonders in Kombination mit vielen Plugins. Hängende Events, die durch fehlgeschlagene Läufe immer wieder neu geplant werden, erzeugen Schleifen, die Logs fluten. Locking-Probleme in der Datenbank zwingen Cronjobs in längere Laufzeiten, wodurch Überschneidungen zunehmen. Außerdem können HTTP-Blockaden (z. B. DNS oder Remote-API) Cronläufe künstlich verlängern und Worker binden. Wer diese Muster kennt, spart viel Zeit bei der Ursachensuche und senkt die Peaks schnell.

Kurz zusammengefasst

Ungleichmäßige CPU-Last in WordPress hat oft ihren Ursprung in WP-Cron, das bei Seitenaufrufen als Trigger wirkt und PHP-Worker bindet. Ich schalte den internen Trigger ab, setze einen System-Cron, optimiere Intervalle und priorisiere SEO-relevante Aufgaben, damit Rendering Vorrang hat. Monitoring mit Logs, WP Crontrol und APM-Analysen zeigt mir fehlerhafte Events, enge Takte und blockierende Prozesse. Für große Projekte verschiebe ich rechenintensive Arbeiten in Queues, um Frontend und Hintergrundjobs sauber zu trennen. Dieser Weg führt zu gleichmäßiger Last, kürzerer TTFB und spürbar schnellerer Auslieferung – ohne unerwartete Spitzen.

Aktuelle Artikel