// <a href="https://hosting-fixers.de/category/pagespeed/" rel="category tag">Pagespeed</a>
WordPress Performance Debugging in der Praxis: Vom Serverload bis zum einzelnen WPML-Filter

Load Average über 20, I/O-Wait über 33 %, 14 blockierte Prozesse – und der erste Verdächtige war natürlich MariaDB. Oder Memcached. Oder zu wenig CPU. Keine dieser Vermutungen stimmte. Dieser Artikel dokumentiert eine reale Performance-Analyse einer großen WordPress-News-Website auf einem produktiven cPanel-Server: von den Messwerten auf Betriebssystemebene bis zum einzelnen WPML-Filter-Callback.
Der erste Blick: Was die Serverlast wirklich bedeutet
Die untersuchte Website – im Folgenden ma**** – läuft auf einem shared cPanel-Server mit AlmaLinux 8, 10 vCPU, ca. 60 GB RAM, MariaDB 10.5.x, PHP 7.4 und Nginx als Reverse Proxy vor Apache mit PHP-FPM. Als Object Cache kommt Memcached mit 4 GB Speicher zum Einsatz.
Regelmäßig traten starke Lastspitzen auf. Ein besonders auffälliger Messzeitpunkt:
| Metrik | Gemessener Wert |
|---|---|
| Load Average (1 min) | 20,28 |
| Blockierte Prozesse (D-State) | 14 |
| CPU iowait | 33,91 % |
| CPU steal | 0,00 % |
| sda await | 81,86 ms |
| sda avgqu-sz (Queue) | 13,30 |
| sda util | 54,42 % |
| RAM-Druck / Swap | praktisch keiner |
Weitere Messpunkte zeigten Storage-Latenzen zwischen 34 und 82 ms mit Queues teilweise über 13. Zu einem anderen Zeitpunkt lag die Last bei ca. 25.
Was Load Average wirklich misst
Linux Load Average zählt alle Prozesse, die entweder aktiv Rechenzeit nutzen oder auf eine Ressource warten – typischerweise Storage-I/O oder Locks. Ein Load Average von 20 auf einem 10-vCPU-System bedeutet deshalb nicht zwingend, dass die CPU zu 200 % ausgelastet ist. Es bedeutet, dass zu jedem Messzeitpunkt durchschnittlich 20 Prozesse nicht sofort weiterlaufen konnten.
Prozesse im sogenannten D-State (uninterruptible sleep) warten auf I/O und können nicht durch Signale unterbrochen werden. 14 solche Prozesse gleichzeitig, kombiniert mit 33,91 % iowait, sind ein klares Indiz: Das System wartet auf Storage, nicht auf freie CPU-Kerne. Wie stark Storage-Latenz WordPress-Systeme bremst, zeigt auch unser VPS-Benchmark mit IOPS-Messwerten.
Warum 60 GB RAM dieses Problem nicht lösen
RAM war reichlich vorhanden, Swap-Druck praktisch nicht vorhanden. Das schließt Speichermangel als Ursache aus. Mehr RAM hilft bei I/O-bedingten Latenzen nur indirekt. Es behebt aber keine strukturell erhöhte Storage-Latenz auf Infrastrukturebene.
CPU-Steal lag bei exakt 0,00 %. Das bedeutet: Der Hypervisor entzog dem System zu diesem Zeitpunkt keine CPU-Zeit für andere VMs. Das spricht gegen klassischen CPU-Steal als Ursache. Es garantiert aber keine insgesamt problemfreie Hosting-Infrastruktur – Storage-Subsysteme auf Shared-Servern können unabhängig davon unter Last geraten. Die Messwerte – await zwischen 34 und 82 ms, Queue-Tiefe über 13 – sind Indizien, keine abschließende Ursachenzuweisung.
Lastverstärker: Automatisierter Traffic
Parallel zur Lastphase wurden automatisierte Browserzugriffe beobachtet. Zwei anonymisierte externe Quellen erzeugten ca. 1.780 bzw. ca. 1.498 Requests pro Stunde, teilweise als Bursts mit über 300 Requests in kurzen Zeitfenstern. User-Agents enthielten unter anderem HeadlessChrome und ähnliche automatisierte Clients.
Ein Teil dieser Zugriffe wurde durch Nginx Rate Limiting bereits mit HTTP 429 (Too Many Requests) begrenzt. HTTP 429 ist kein dauerhafter Bann – es ist eine temporäre Ratenbegrenzung. Die Security-Schicht war bereits behandelt und war ein Lastverstärker, aber nicht die einzige Ursache des Problems.
Die WordPress-Architektur: WPBakery-Grids und Cache-Warmup
Die Homepage von ma**** enthielt zum Untersuchungszeitpunkt:
| Element | Anzahl |
|---|---|
| vc_basic_grid | 19 |
| vc_empty_space | 51 |
| vc_column_text | 34 |
| vc_column_inner | 32 |
| vc_row_inner | 30 |
| vc_column | 28 |
| vc_row | 22 |
Insbesondere die 19 vc_basic_grid-Elemente sind problematisch. Jedes WPBakery-Grid kann – sofern nicht vollständig gecacht – einen eigenen AJAX-Request auslösen, der einen vollständigen WordPress-Bootstrap inklusive Datenbankabfragen, PHP-Ausführung und Object-Cache-Zugriffen verursacht. 19 Grids auf einer Seite können bei einem einzigen Seitenaufruf bis zu 19 parallele PHP-FPM-Prozesse erzeugen.
Es existierten bereits kundenspezifische MU-Plugins für Grid-Caching und Cache-Warmup. Beim Warmup kann es passieren, dass Cache-Einträge gelöscht und für alle 19 Grids gleichzeitig neu aufgebaut werden – und damit exakt die Lastspitze erzeugt wird, die der Warmup vermeiden soll.
Memcached: technisch einwandfrei – und trotzdem ein Hinweis
Memcached war mit 4 GB Speicher, 2.000 Connections, 4 Threads und deaktiviertem UDP konfiguriert. Die gemessenen Werte:
| Metrik | Wert |
|---|---|
| cmd_get | > 371 Mio. |
| cmd_set | > 32 Mio. |
| Hits | > 366 Mio. |
| Misses | ca. 4,26 Mio. |
| Hit Rate | ca. 98,8 % |
| Genutzter Speicher | ca. 763 MB / 4 GB |
| Items | ca. 1,85 Mio. |
| Evictions | 0 |
| Rejected Connections | 0 |
Eine Hit Rate von 98,8 % bei null Evictions zeigt: Memcached funktioniert technisch einwandfrei. Trotzdem bedeutet das nicht, dass die Anwendung effizient mit dem Cache umgeht. Die entscheidende Frage ist: Wie viele Cache-Operationen verursacht ein einzelner Request?
strace auf einem PHP-FPM-Worker: was wirklich passiert
Für einen realen Artikelrequest wurde ein laufender PHP-FPM-Worker mit strace untersucht. Die File Descriptors konnten zugeordnet werden: einer zur MariaDB-Verbindung, einer zur Memcached-TCP-Verbindung auf localhost.
| Syscall | Calls | Zeit (gesamt) |
|---|---|---|
| poll | 268 | ca. 1,025 s |
| sendto | 820 | ca. 0,847 s |
| recvfrom | 1.045 | ca. 0,696 s |
| read | 624 | ca. 0,096 s |
| write | 72 | ca. 0,030 s |
| Dienst | Calls | Kumulierte Syscall-Zeit |
|---|---|---|
| MariaDB | 311 | ca. 0,683 s |
| Memcached | 1.823 | ca. 1,887 s |
Wichtiger methodischer Hinweis: Diese Zahlen sind keine Service-Latenzen. strace misst Syscall-Zeiten auf Kernel-Ebene. Die hohe kumulierte Zeit bei Memcached ergibt sich aus der sehr viel höheren Anzahl an Operationen: 1.823 Cache-Calls gegenüber 311 Datenbankoperationen. Ein einzelner dynamischer Request erzeugt auf diesem System fast 2.000 Netzwerk-Syscalls – ein Muster, das auf WordPress-/Plugin-Ebene genauer untersucht werden sollte.
Zusätzlich fiel im Trace ein sehr großer WPML-bezogener Cachewert im Kontext von WPML_404_Guess auf – ca. 925 KB. Kein Beweis für eine Fehlfunktion, aber ein weiterer Hinweis zur Untersuchung von WPML.
WP-CLI profile: Hotspots auf Callback-Ebene
Zur weiteren Analyse wurde wp-cli/profile-command Version 2.1.5 eingesetzt. Wichtig: opcache.enable_cli = Off. WP-CLI läuft ohne OPcache – absolute Zeitwerte sind höher als in der PHP-FPM-Produktionsumgebung, zwischen Läufen treten starke Schwankungen auf. WP-CLI-Profilwerte dürfen nicht 1:1 mit PHP-FPM-Webrequest-Zeiten gleichgesetzt werden.
| Callback | Gemessene Zeit | SQL Queries |
|---|---|---|
| WPML_URL_Filters->home_url_filter() | ca. 345 ms | 0 |
| Redux::createRedux() (EasyWeb-Theme) | ca. 260 ms | – |
| RankMathPro->init() | ca. 113 ms | – |
| Rank Math Replace Variables | ca. 105 ms | – |
| SitePress->init() | ca. 93–785 ms* | – |
| Vc_Manager->init() (WPBakery) | ca. 23 ms | – |
* SitePress: stark schwankend, nicht stabil reproduzierbar. Vc_Manager->init() bei 23 ms bedeutet nicht, dass WPBakery-Grids harmlos sind – der Plugin-Bootstrap ist nicht das Problem, die Grid-Last entsteht beim Rendering und durch AJAX-Requests.
Warum home_url() bei WPML teuer werden kann
Der auffälligste Hotspot ohne SQL-Queries war WPML_URL_Filters->home_url_filter(). WPML registriert diesen Callback mit Priorität -10:
add_filter( 'home_url', [ $this, 'home_url_filter' ], -10, 4 ); Jeder Aufruf von home_url() durchläuft damit: language_negotiation_type lesen, WPML_Home_Url_Filter_Context erzeugen, should_not_filter() prüfen, aktuelle Request-Sprache bestimmen, URL konvertieren, wpml_get_home_url-Filter ausführen. Das alte EasyWeb-Theme rief home_url() an zahlreichen Stellen auf – Theme-Funktionen, Suchformular, Header-Varianten, Logos, Redux-Helfer, Live Search, Breadcrumbs, MU-Plugins. Dieselbe URL wurde innerhalb eines einzigen PHP-Requests wiederholt durch die WPML-Verarbeitungskette geschickt, ohne Zwischenspeicherung des Ergebnisses.
0 SQL Queries bei 345 ms zeigt: Das Problem liegt nicht in der Datenbank, sondern in der Verarbeitungslogik multipliziert mit der Aufrufhäufigkeit. Das ist ein klassisches Performance-Antipattern: Eine Funktion mit überschaubaren Einzelkosten wird so oft pro Request aufgerufen, dass die kumulative Ausführungszeit signifikant wird.
Der Quick-Fix: Request-Local Memoization
Verändert wurde keine WPML-Core-Datei. Stattdessen wurde über ein MU-Plugin der originale WPML-Callback durch einen Wrapper mit Request-lokalem Caching ersetzt:
// Konzept – kein produktionsreifer Code ohne Anpassung // Nur nach Backup und Staging-Test einsetzen // WPML-Updates können interne Signaturen ändern add_filter( 'home_url', function( $url, $path, $scheme, $blog_id ) use ( $original_callback ) { $lang = apply_filters( 'wpml_current_language', null ); $key = md5( $lang . '|' . $path . '|' . $scheme . '|' . (int) $blog_id ); static $cache = []; if ( isset( $cache[ $key ] ) ) { return $cache[ $key ]; } $result = call_user_func( $original_callback, $url, $path, $scheme, $blog_id ); $cache[ $key ] = $result; return $result; }, -10, 4 ); Der Cache gilt ausschließlich für den aktuellen PHP-Request. Bei Cache-Miss wird die originale WPML-Logik vollständig ausgeführt. Kein persistenter Cache, kein Netzwerkoverhead, kein Invalidierungsproblem. Multisite, REST-API, AJAX und Language Switcher müssen bei produktivem Einsatz gesondert berücksichtigt werden.
Messergebnis des Quick-Fixes
| Messzeitpunkt | WPML_URL_Filters->home_url_filter() |
|---|---|
| Vor dem Wrapper | ca. 345 ms |
| Nach dem Wrapper | ca. 103 ms |
| Reduktion in diesem Lauf | ca. 70 % |
In diesem konkreten Profiling-Lauf sank die gemessene Zeit dieses Callbacks von rund 345 ms auf rund 103 ms. Ein A/B-Test mit vollständig deaktiviertem WPML-home_url-Filter zeigte eine Gesamtlaufzeit von ca. 2,71 s (gegenüber ca. 4,02 s im Basislauf) – als Richtungsindikator, nicht als exakter Produktionswert.
Was bewusst nicht gemacht wurde
In einem Profiling-Lauf erschien SitePress->init() mit ca. 785 ms. Weitere Läufe: ca. 155 ms, ca. 97 ms, ca. 91 ms. Nicht reproduzierbar – kein Patch. Ein Lauf zeigte RankMath\Redirections\Redirections->do_redirection() mit ca. 489 ms, andere Läufe 42–71 ms. Die Datenbank enthielt 1.094 aktive Redirects – nicht deaktiviert. Außerdem nicht gemacht: WPML deaktivieren, Memcached abschalten, MariaDB pauschal mehr RAM zuweisen, Plugins aufgrund einzelner Profiler-Läufe entfernen, den produktiven Server mit Dauerbenchmarks belasten.
Die wichtigsten Erkenntnisse
- Load Average allein sagt nichts über CPU-Auslastung. Blockierte D-State-Prozesse erhöhen den Load ohne CPU zu verbrauchen.
- Hoher I/O-Wait mit Storage-Latenzen von 34–82 ms bremst PHP-FPM massiv, auch wenn RAM und CPU ausreichend wären.
- 60 GB RAM kompensieren keine Storage-Latenz.
- Eine Hit Rate von 98,8 % bedeutet nicht, dass die Anwendung effizient ist. 1.823 Cache-Calls pro Request sind unabhängig davon ein Muster, das analysiert werden sollte.
- Tausende schnelle Cache-Operationen summieren sich unter Last.
- strace und WordPress-Profiler beantworten unterschiedliche Fragen und ergänzen sich.
- WP-CLI ohne CLI-OPcache darf nicht mit PHP-FPM verglichen werden.
- Plugin-Bootstrap-Zeit und tatsächliche Workload sind verschieden.
- Häufig wiederholte kleine Funktionen können größere Bottlenecks erzeugen als einzelne SQL-Queries.
- Request-local Memoization ist bei deterministischen Berechnungen extrem günstig.
- Ein einzelner schlechter Profiling-Wert reicht nicht für einen riskanten Patch. Reproduzierbarkeit zuerst.
- Performance bedeutet nicht alles abzuschalten. 1.094 aktive Redirects sind eine Funktion, kein Fehler.
- Bei Legacy-Systemen ist nicht jede mögliche Optimierung wirtschaftlich. Pareto-Prinzip anwenden.
- Ein geplanter Relaunch verändert die Strategie – Quick-Wins statt riskante Tiefenoptimierung.
Fazit
Der Load Average von 20 auf ma**** war nicht die CPU, nicht Memcached und nicht zu wenig RAM. Es war eine Kombination: Storage-Latenz auf Infrastrukturebene, automatisierte Zugriffe als Lastverstärker, ein cache-intensives Request-Muster mit fast 2.000 Syscalls pro Request und ein WPML-Filter-Callback ohne Memoization, der dutzende Male pro Request durchlief. Die Lösung war kein Pauschalupgrade – es war eine systematische Analyse von der Betriebssystemebene bis zum einzelnen Callback. Wenn deine WordPress-Website unter unerklärlicher Last leidet, lohnt sich eine strukturierte Performance-Analyse weit vor jedem Serverupgrade. Das ist der Ansatz von Hosting-Fixers.
Häufige Fragen
Was bedeutet hoher I/O-Wait bei WordPress?
I/O-Wait zeigt an, wie viel Prozent der CPU-Zeit damit verbracht wird, auf Storage-Zugriffe zu warten. PHP-FPM-Prozesse warten auf Datenbankzugriffe oder Filesystem-Operationen, die Storage-seitig verzögert werden. Mehr RAM oder CPU lösen dieses Problem nicht.
Warum ist WordPress Load Average hoch, obwohl die CPU nicht ausgelastet ist?
Linux Load Average zählt alle wartenden und laufenden Prozesse. Prozesse im D-State erhöhen den Load ohne CPU zu verbrauchen. Ein Load Average von 20 auf einem 10-vCPU-System kann vollständig durch blockierte I/O-Prozesse entstehen.
Ist eine Memcached-Hit-Rate von 98 % gut für WordPress?
Technisch ja. Trotzdem ist die Anzahl der Cache-Operationen pro Request entscheidend. Wenn ein Request fast 2.000 Cache-Calls erzeugt, summiert sich das unter Last. Eine hohe Hit Rate ist kein Freifahrtschein für beliebig viele Cache-Zugriffe.
Kann WP-CLI profile für PHP-FPM-Zeiten verwendet werden?
Nur bedingt. WP-CLI läuft typischerweise ohne OPcache, wodurch absolute Zeitwerte höher sind. Relative Vergleiche zwischen Callbacks bleiben aussagekräftig.
Was ist Request-Local Memoization und wann hilft sie bei WordPress?
Request-Local Memoization speichert das Ergebnis einer Funktion für einen einzigen PHP-Request in einem statischen PHP-Array. Bei deterministischen Funktionen, die mehrfach mit denselben Parametern aufgerufen werden, spart das die Wiederholungsverarbeitung – ohne persistenten Cache, ohne Invalidierungsproblem, ohne Netzwerkoverhead.
Autor: Manuel Kuhn, WordPress Performance Engineer & Linux-Serveradministrator, Hosting-Fixers.de – August 2026