Utilisation du CPU à 100 % sous WordPress : trouvez le plugin ou le bot qui surcharge votre site

Cet article vous a aidé ?

Quand WordPress coince un cœur à 100 %, ce n’est pas de la « tuning de performance ». On a l’impression que le site fond en direct pendant que vous devez expliquer à quelqu’un qui n’est pas technique que oui, le serveur est « up », mais non, il ne fonctionne pas réellement.

La bonne nouvelle : les pics CPU sont généralement diagnostiquables. La mauvaise nouvelle : on devine souvent. On désactive des plugins au hasard, on redémarre PHP-FPM comme un rituel, et on se croit sorti d’affaire jusqu’au prochain pic. Faisons cela comme on gère un système en production : mesurer, attribuer, décider, puis corriger sans casser le tunnel de paiement.

Playbook de diagnostic rapide (faire ça en priorité)

Ceci est le chemin « arrêter l’hémorragie et identifier l’assaillant ». N’optimisez pas. Ne refactorez pas. Ne discutez pas avec le graphique. Suivez la chaîne de preuves du CPU vers le process, la requête, puis le code.

1) Confirmer qu’il s’agit bien d’une saturation CPU (et pas seulement de la « charge »)

  • Vérifiez CPU, file d’attente exécutable, temps volé (les VM mentent quand les voisins sont bruyants).
  • Décision : si le temps volé est élevé, votre appli peut être innocente ; c’est l’hyperviseur qui pose problème.

2) Identifier quelle famille de processus consomme le CPU

  • S’agit-il des workers php-fpm ? D’un process hors contrôle ? De mysqld ? D’autre chose ?
  • Décision : si PHP est chaud, l’étape suivante est l’attribution par requête. Si MySQL est chaud, passez aux requêtes lentes.

3) Attribuer le CPU à un chemin de requête et à un client

  • Corrélez le slowlog PHP-FPM, les logs d’accès Nginx/Apache et les IP/URL en tête.
  • Décision : si une URL ou une IP domine, limitez au bord (WAF/rate limit) pendant que vous creusez.

4) Trouver le point d’entrée WordPress

  • Coupables fréquents : wp-login.php, xmlrpc.php, /wp-json/, admin-ajax.php, endpoints WooCommerce, recherche, génération de sitemaps.
  • Décision : si ce sont des endpoints d’authentification, traitez-les comme du trafic hostile. Si c’est admin-ajax, traitez-le comme comportement plugin/thème jusqu’à preuve du contraire.

5) Identifier le plugin/thème ou la requête

  • Utilisez les backtraces du slowlog PHP-FPM et le slow query log MySQL.
  • Décision : désactivez/remplacez le plugin, corrigez la config ou mettez en cache de façon agressive — sur la base des preuves, pas des impressions.

6) Appliquer une limite temporaire sûre

  • Rate-limitez les chemins abusifs, activez le cache et réglez des limites PHP-FPM raisonnables.
  • Décision : préférez une dégradation contrôlée (429 sur les endpoints abusifs) plutôt que la mort totale du site.

Ce que signifie vraiment « 100 % CPU » sur WordPress

Dans l’univers WordPress, « CPU à 100 % » signifie généralement que PHP fait trop de travail trop souvent. Cela peut aussi vouloir dire trop de workers PHP sont prêts à s’exécuter en même temps parce qu’un plugin a déclenché des requêtes coûteuses ou des appels distants. Ou bien MySQL est en feu et PHP attend, mais vos graphiques sont trop grossiers pour le montrer.

Quelques réalités :

  • Un cœur saturé sur une machine multi-cœur peut quand même être catastrophique si PHP-FPM est mono-thread par requête et que votre endpoint le plus chaud sérialise le travail (verrous, sessions, stampedes de cache).
  • Moyenne de charge n’est pas CPU. La charge peut être attente I/O, file d’exécution, ou processus bloqués. Vérifiez iowait et la file d’exécution.
  • « Mais on a du cache » ne signifie pas que vous êtes à l’abri. Les utilisateurs connectés, le panier/checkout, admin-ajax et les pages personnalisées contournent la plupart des caches de page par conception.

Idée paraphrasée de Werner Vogels (CTO d’Amazon) : Tout finit par tomber en panne ; concevez pour détecter, limiter le rayon des dégâts et récupérer rapidement.

Et oui, parfois la cause est ridiculement banale : une tâche cron qui tourne chaque minute parce que quelqu’un a copié un extrait « performance » sur un forum.

Blague courte #1 : WordPress ne « monte pas au hasard » à 100 % CPU. Il est simplement très engagé dans le chaos que vous avez autorisé.

Faits intéressants et contexte (histoire courte et utile)

  1. WordPress lancé en 2003 comme fork de b2/cafelog ; il a hérité du modèle « PHP rend tout à la requête » qui fait que le coût par requête compte.
  2. wp-cron n’est pas un vrai cron par défaut. Il est déclenché par le trafic du site, ce qui signifie que « plus de visiteurs » peut accidentellement dire « plus d’exécutions de cron ».
  3. xmlrpc.php était une compatibilité utile (publication à distance, clients mobiles), puis il est devenu une cible privilégiée pour le credential stuffing et l’amplification pingback.
  4. admin-ajax.php est devenu un couteau suisse pour les plugins parce que c’était pratique ; c’est aussi un générateur de charge involontaire quand il est utilisé pour un polling intensif côté front.
  5. PHP-FPM a remplacé mod_php comme pattern de déploiement courant car il isole les pools et améliore la stabilité, mais il a aussi rendu les erreurs de « max_children » plus faciles à commettre à grande échelle.
  6. Le cache d’objets a déplacé les goulots d’étranglement : ajouter Redis/Memcached peut réduire la charge MySQL, mais cela peut aussi cacher du code mauvais jusqu’à ce que des misses ou évictions provoquent des stampedes.
  7. WooCommerce a changé la forme du trafic : paniers, sessions, fragments AJAX et comportements connectés invalident le cache de page complet plus qu’un blog classique.
  8. HTTP/2 a réduit le coût des connexions, mais il peut augmenter la concurrence des requêtes, ce qui fait échouer plus vite des endpoints coûteux si vous ne limitez pas le débit.
  9. Le WordPress « headless » a augmenté l’usage de l’API : un trafic massif sur /wp-json/ peut agir comme un DDoS de faible intensité si les requêtes sont non bornées ou non mises en cache.

Tâches pratiques : commandes, sorties, décisions (la trousse d’outils)

Vous voulez des tâches reproductibles qui vous emmènent de « CPU mauvais » à « ce plugin et ce endpoint, depuis ces IPs, à ce rythme ». Ci-dessous des tâches à lancer sur un VPS/VM Linux typique avec Nginx/Apache + PHP-FPM + MySQL/MariaDB. Chaque tâche inclut : commande, sortie exemple, ce que cela signifie et la décision à prendre.

Task 1: Confirm CPU saturation vs steal time

cr0x@server:~$ mpstat -P ALL 1 5
Linux 6.5.0 (wp-prod-01)  12/27/2025  _x86_64_  (4 CPU)

12:01:11 PM  CPU   %usr %nice  %sys %iowait  %irq %soft  %steal %idle
12:01:12 PM  all  92.10  0.00  6.40   0.20   0.00  0.80   0.00  0.50
12:01:12 PM    0  99.00  0.00  1.00   0.00   0.00  0.00   0.00  0.00
12:01:12 PM    1  88.00  0.00 11.00   1.00   0.00  0.00   0.00  0.00
12:01:12 PM    2  92.00  0.00  7.00   1.00   0.00  0.00   0.00  0.00
12:01:12 PM    3  89.00  0.00 10.00   1.00   0.00  0.00   0.00  0.00

Sens : Un %usr élevé avec %steal proche de zéro signifie que votre charge consomme réellement du CPU. Un %idle faible confirme la saturation.

Décision : Continuez l’investigation côté application. Si %steal était élevé (>5–10%), escaladez auprès de l’hébergeur ou migrez.

Task 2: Find which processes are consuming CPU right now

cr0x@server:~$ ps -eo pid,ppid,cmd,%cpu,%mem --sort=-%cpu | head -n 12
  PID  PPID CMD                         %CPU %MEM
19421 19310 php-fpm: pool www           88.4  2.1
19455 19310 php-fpm: pool www           72.9  2.0
19470 19310 php-fpm: pool www           65.1  2.1
19310     1 php-fpm: master process      2.1  0.4
 2214     1 nginx: worker process        1.3  0.3
 1870     1 mysqld                       0.9 10.5

Sens : Ce sont les workers PHP-FPM qui brûlent, pas Nginx. MySQL n’est pas (encore) le point chaud principal.

Décision : Passez à l’attribution des requêtes PHP : slowlog, logs d’accès et timings upstream.

Task 3: Check PHP-FPM pool saturation (are you queueing?)

cr0x@server:~$ sudo ss -s
Total: 817 (kernel 0)
TCP:   613 (estab 402, closed 162, orphaned 0, synrecv 0, timewait 162/0), ports 0

Transport Total     IP        IPv6
RAW       0         0         0
UDP       6         4         2
TCP       451       380       71
INET      457       384       73
FRAG      0         0         0

Sens : Beaucoup de sessions TCP établies peuvent indiquer un trafic normal… ou des réponses lentes qui s’empilent.

Décision : Vérifiez ensuite les timings upstream Nginx et le statut PHP-FPM.

Task 4: Enable and read PHP-FPM status (if available)

cr0x@server:~$ curl -s http://127.0.0.1/fpm-status | head
pool:                 www
process manager:      dynamic
start time:           27/Dec/2025:11:02:13 +0000
start since:          3540
accepted conn:        918245
listen queue:         37
max listen queue:     221
listen queue len:     128

Sens : Une listen queue non nulle signifie que des requêtes attendent un worker PHP libre. C’est une latence visible par l’utilisateur.

Décision : Vous pouvez temporairement augmenter pm.max_children si la RAM le permet, mais trouvez d’abord pourquoi les workers sont lents (code coûteux, appels distants, BD). Augmenter la capacité d’un travail mauvais ne fait que multiplier le travail mauvais.

Task 5: Check Nginx top URLs by request rate

cr0x@server:~$ sudo awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head
  9412 /wp-admin/admin-ajax.php
  3120 /wp-login.php
  1788 /wp-json/wp/v2/posts?per_page=100
   904 /?s=shoes
   611 /xmlrpc.php

Sens : admin-ajax.php domine. Ce n’est rarement du « navigateur normal ». C’est le plus souvent une fonctionnalité de plugin, un script de thème, ou un bot qui sonde des endpoints.

Décision : Identifiez quel action= est chaud, puis mappez-le à un plugin.

Task 6: Break down admin-ajax by action parameter

cr0x@server:~$ sudo grep "admin-ajax.php" /var/log/nginx/access.log | awk -F'action=' '{print $2}' | awk '{print $1}' | cut -d'&' -f1 | sort | uniq -c | sort -nr | head
  8122 wc_fragment_refresh
   901 elementor_ajax
   214 wpforms_submit
   143 heartbeat

Sens : wc_fragment_refresh est l’appel classique de rafraîchissement des fragments de panier WooCommerce. Cela peut être légitime, mais c’est aussi notoirement bavard et hostile au cache.

Décision : Si le trafic vient de vrais acheteurs, optimisez le parcours Woo et les règles de cache. Si ce sont des bots, rate-limitez/refusez selon le comportement.

Task 7: Identify top client IPs hammering the endpoint

cr0x@server:~$ sudo grep "admin-ajax.php" /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -nr | head
  3022 203.0.113.50
  1877 198.51.100.77
   944 192.0.2.19
   611 10.0.0.12

Sens : Une IP générant des milliers de requêtes est suspecte à moins que ce ne soit votre monitoring, un test de charge ou un reverse proxy connu.

Décision : Si ce ne sont pas vos proxies, bloquez ou rate-limitez immédiatement au bord ou au firewall. Poursuivez ensuite le diagnostic pour le reste du trafic.

Task 8: Confirm whether requests are slow in Nginx upstream timing

cr0x@server:~$ sudo awk '{print $(NF-1),$7,$1}' /var/log/nginx/access.log | head -n 5
0.842 /wp-admin/admin-ajax.php 203.0.113.50
1.102 /wp-admin/admin-ajax.php 203.0.113.50
0.019 /wp-login.php 198.51.100.77
0.611 /wp-admin/admin-ajax.php 192.0.2.19
0.955 /wp-json/wp/v2/posts?per_page=100 198.51.100.77

Sens : Si vous avez configuré un format de log incluant le temps de réponse upstream, vous pouvez voir si le backend est lent. Des valeurs proches de 1s pour admin-ajax à grande échelle vont cuire le CPU.

Décision : Un backend lent signifie qu’il vous faut slowlog PHP + profiling WP, pas seulement du blocage.

Task 9: Enable PHP-FPM slowlog and catch stack traces

cr0x@server:~$ sudo grep -nE "request_slowlog_timeout|slowlog" /etc/php/8.2/fpm/pool.d/www.conf
308:request_slowlog_timeout = 5s
309:slowlog = /var/log/php-fpm/www-slow.log

Sens : Les requêtes prenant plus de 5 secondes vont dumper une backtrace dans le fichier slowlog.

Décision : Activez-le pendant une fenêtre d’incident. Si vous ne pouvez pas supporter la surcharge de logging, mettez-le plus haut (10–15s) et gardez-le temporaire.

cr0x@server:~$ sudo tail -n 25 /var/log/php-fpm/www-slow.log
[27-Dec-2025 12:03:41]  [pool www] pid 19455
script_filename = /var/www/html/wp-admin/admin-ajax.php
[0x00007f2a1c8b2a40] mysqli_query() /var/www/html/wp-includes/wp-db.php:2345
[0x00007f2a1c8b28b0] _do_query() /var/www/html/wp-includes/wp-db.php:2263
[0x00007f2a1c8b27f0] query() /var/www/html/wp-includes/wp-db.php:3307
[0x00007f2a1c8b2460] get_results() /var/www/html/wp-includes/wp-db.php:3650
[0x00007f2a1c8b1f90] wc_get_products() /var/www/html/wp-content/plugins/woocommerce/includes/wc-product-functions.php:1201
[0x00007f2a1c8b1c00] my_custom_fragments() /var/www/html/wp-content/plugins/some-fragments/plugin.php:88

Sens : Vous avez maintenant un chemin du endpoint chaud vers un fichier et une fonction de plugin spécifique. C’est de l’or.

Décision : Désactivez/remplacez le plugin ou ajustez ses réglages. Si c’est votre code personnalisé, corrigez-le. Si c’est le comportement du core WooCommerce, pensez à des ajustements de cache de fragments ou à réduire les appels.

Task 10: Use WP-CLI to list plugins and their status

cr0x@server:~$ cd /var/www/html && sudo -u www-data wp plugin list --status=active
+---------------------+----------+-----------+---------+
| name                | status   | update    | version |
+---------------------+----------+-----------+---------+
| woocommerce         | active   | available | 8.9.2   |
| elementor           | active   | none      | 3.24.0  |
| some-fragments      | active   | none      | 1.6.1   |
| redis-cache         | active   | none      | 2.5.3   |
+---------------------+----------+-----------+---------+

Sens : Confirme que le plugin suspect est actif et identifie l’état des mises à jour (parfois vous êtes sur une version connue pour être problématique).

Décision : Si une mise à jour existe et que le changelog indique des corrections de perf/ sécurité, planifiez-la. Si vous avez besoin d’un soulagement immédiat, désactivez l’auteur hors-pointe ou sur un canary.

Task 11: Temporarily disable a suspected plugin (controlled test)

cr0x@server:~$ cd /var/www/html && sudo -u www-data wp plugin deactivate some-fragments
Plugin 'some-fragments' deactivated.

Sens : Retire le chemin de code de la production sans éditer les fichiers directement.

Décision : Surveillez le CPU et le taux de requêtes. Si le CPU chute et que les erreurs n’augmentent pas, vous avez trouvé le coupable. Si les erreurs augmentent, restaurez et tentez une atténuation plus ciblée (rate limit, cache, feature toggle).

Task 12: Check MySQL for slow queries (is DB the real bottleneck?)

cr0x@server:~$ sudo mysql -e "SHOW FULL PROCESSLIST\G" | head -n 30
*************************** 1. row ***************************
     Id: 12891
   User: wpuser
   Host: 127.0.0.1:51862
     db: wordpress
Command: Query
   Time: 9
  State: Sending data
   Info: SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE 1=1 AND wp_posts.post_type IN ('product') AND (wp_posts.post_status = 'publish') ORDER BY wp_posts.post_date DESC LIMIT 0, 48

Sens : Des requêtes pendant 9 secondes ne sont pas « ok ». « Sending data » implique souvent des scans massifs, des index manquants ou des ensembles de résultats énormes.

Décision : Activez le slow query log, inspectez les motifs de requêtes et corrigez les index ou réduisez le coût des requêtes (pagination, contraintes, cache). Pensez à limiter les API coûteuses.

Task 13: Turn on MySQL slow query log (temporary) and inspect

cr0x@server:~$ sudo mysql -e "SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 1;"
cr0x@server:~$ sudo tail -n 20 /var/log/mysql/slow.log
# Time: 2025-12-27T12:06:01.123456Z
# User@Host: wpuser[wpuser] @ localhost []
# Query_time: 2.941  Lock_time: 0.001 Rows_sent: 48  Rows_examined: 504812
SET timestamp=1766837161;
SELECT SQL_CALC_FOUND_ROWS wp_posts.ID FROM wp_posts WHERE 1=1 AND wp_posts.post_type IN ('product') ORDER BY wp_posts.post_date DESC LIMIT 0, 48;

Sens : Des Rows_examined dans les centaines de milliers pour un petit résultat de page est le classique « vous scannez l’océan pour trouver un poisson ».

Décision : Identifiez quelle page/API déclenche cela, puis envisagez une réécriture de requête via les réglages du plugin, l’ajout d’index (avec prudence) ou la mise en cache au niveau objet/page.

Task 14: Check disk I/O wait (CPU might be “busy waiting” elsewhere)

cr0x@server:~$ iostat -x 1 3
Device            r/s   w/s  rkB/s  wkB/s  await  %util
nvme0n1          2.1  18.2   44.0  512.0   1.20  22.5

Sens : Un await faible et un %util modéré signifient que le stockage n’est probablement pas le facteur limitant. Si await est élevé (dizaines/centaines de ms) et %util est saturé, votre « problème CPU » est en réalité une pression I/O.

Décision : Si le stockage est le goulot, réglez l’I/O (optimisez la BD, passez à un disque plus rapide, réduisez le logging, ajustez le buffer pool) avant de toucher à PHP.

Task 15: Catch a live PHP worker backtrace with perf (when you’re desperate)

cr0x@server:~$ sudo perf top -p 19421
Samples: 2K of event 'cpu-clock', 4000 Hz, Event count (approx.): 510000000
Overhead  Shared Object        Symbol
  18.20%  php-fpm8.2           zend_execute_ex
  11.35%  php-fpm8.2           zif_preg_match
   8.74%  libpcre2-8.so.0.11.2 pcre2_match_8
   6.10%  php-fpm8.2           zim_spl_autoload_call

Sens : Beaucoup de regex (preg_match) dans PHP peut être du comportement de plugin (filtrage, parsing de contenu, scan de sécurité) ou un thème qui fait des choses « intelligentes » par requête.

Décision : Combinez avec le slowlog pour pointer quel plugin appelle ça. Si un plugin de sécurité scanne chaque requête, il est peut-être temps de choisir un produit plus calme.

Est-ce un plugin, un bot ou la plateforme ?

La plupart des incidents CPU entrent dans trois catégories :

  • Trafic hostile : credential stuffing sur wp-login.php, abus XML-RPC, bruteforce sur endpoints REST, scraping agressif, ou des « outils SEO » qui se comportent comme des locustes.
  • Comportement plugin/thème : polling admin-ajax, requêtes produits coûteuses, traitement d’images, balises d’analyse, constructeurs de pages qui rendent côté serveur ou génèrent du CSS dynamique.
  • Contraintes plateforme : trop peu de cœurs CPU, RAM insuffisante (thrash de swap), steal time d’un voisin, PHP-FPM mal configuré, MySQL sous-dimensionné.

Le mouvement clé est d’attribuer par chemin de requête et par client. Un problème de plugin se manifeste par de nombreuses IPs frappant le même endpoint. Un problème de bot montre souvent un petit ensemble d’IPs ou d’ASNs, des user-agents bizarres, ou des taux de requêtes élevés avec peu de continuité de session.

Le split test en deux minutes

Si l’endpoint chaud est wp-login.php ou xmlrpc.php, supposez du trafic hostile jusqu’à preuve du contraire. Si l’endpoint chaud est admin-ajax.php avec une action constante, supposez plugin/thème jusqu’à preuve du contraire. Si c’est /wp-json/ avec per_page=100 ou des requêtes non bornées, supposez un « problème de conception API ».

Blague courte #2 : Si votre « automation marketing » frappe admin-ajax.php 50 fois par seconde, ce n’est pas de l’automatisation ; c’est un petit déni de service avec du code budgétaire.

Trois mini-histoires du monde corporate (ce qui arrive vraiment)

Mini-histoire 1 : L’incident causé par une mauvaise hypothèse

Une entreprise de taille moyenne utilisait WordPress comme vitrine publique d’une suite produit. Leur rotation SRE le considérait comme « plutôt statique », donc ils ont mis un CDN devant, activé le cache de page, et ont passé à autre chose. Tout le monde dormait mieux. Puis un mardi matin est arrivé avec une odeur familière : des graphiques en falaise.

Le CPU était saturé, la file d’attente PHP-FPM augmentait et la page d’accueil était lente. La première hypothèse fut classique : « Le CDN doit être en panne. » Ce n’était pas le cas. Le taux de hit du cache était excellent. C’est ce qui rendait l’incident déroutant : les pages publiques étaient mises en cache, alors pourquoi PHP mourait-il ?

La réponse était dans les logs d’accès. Un botnet martelait /wp-json/wp/v2/users et /wp-json/wp/v2/posts avec de grandes tailles de page et des paramètres de recherche. Le CDN leur laissait passer les requêtes parce qu’elles ressemblaient à des GET « normaux ». L’équipe avait supposé que « GET est sûr » et que « CDN == bouclier ». Aucune des deux n’était vraie.

La correction fut ennuyeuse et efficace : rate-limit des endpoints REST au bord, en-têtes de cache plus stricts là où c’était sûr, et blocage des endpoints d’énumération d’utilisateurs. Ils ont aussi limité per_page et désactivé des routes inutiles. Le CPU a chuté instantanément, et la leçon est restée : la surface d’attaque inclut tous les endpoints dynamiques en lecture, pas seulement les formulaires de login.

Mini-histoire 2 : L’optimisation qui s’est retournée contre eux

Une autre organisation avait des pics CPU récurrents lors de lancements de campagne. Quelqu’un a proposé un « gain simple » : augmenter pm.max_children de PHP-FPM et les connexions MySQL pour que le serveur « gère plus de concurrence ». Ça sonnait raisonnable. Les graphiques se sont améliorés environ une heure.

Puis le site est devenu plus lent. Pas juste plus lent : erratique. Certaines requêtes répondaient vite ; d’autres prenaient des âges. Le CPU restait élevé, la charge moyenne montait, et MySQL commençait à afficher des requêtes longues. Finalement la machine a commencé à swapper, et le kernel a tué des processus. L’équipe avait réussi à amplifier leur goulot en un effondrement global.

Cause racine : ils ont augmenté la concurrence PHP sans réduire le coût par requête. Plus de workers signifiait plus de requêtes simultanées coûteuses, ce qui a saturé le buffer pool de la BD. La pression mémoire a poussé le système en swap, transformant un « problème CPU » en « problème total ».

La vraie correction était moins glamour : limiter les workers PHP pour tenir la RAM, activer le slowlog PHP-FPM, identifier le plugin générant des requêtes produits non indexées, et changer son comportement. Après ça, ils ont pu augmenter la concurrence prudemment. Ils ont appris à la dure que « plus de workers » n’est pas une optimisation ; c’est un multiplicateur.

Mini-histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la situation

Une marque mondiale avait une instance WordPress pour un grand événement presse. Ils avaient été brûlés auparavant, alors ils suivaient un régime terre-à-terre : inventaire hebdomadaire des plugins, mises à jour en staging, tests de performance de base, et un runbook standard « pic de trafic ». Pas d’héroïsme, juste des habitudes.

Le jour de l’événement, le CPU a monté comme prévu. Puis il a monté davantage. Ils n’ont pas paniqué. Le runbook commençait par : identifier le endpoint chaud, les IPs en tête, la file PHP-FPM, les requêtes lentes MySQL. En quelques minutes ils ont trouvé une hausse de requêtes vers un endpoint de recherche qui contournait le cache de page. C’était du vrai trafic utilisateur, pas des bots.

Parce qu’ils avaient des atténuations préconstruites, ils ont activé une couche de résultats de recherche mise en cache (TTL court), réduit temporairement la complexité de la recherche (moins de champs), et appliqué une limite de débit pour les comportements abusifs. Le site est resté up. Personne n’a remarqué sauf ceux qui surveillaient les graphiques.

La pratique qui a sauvé n’était pas un outil ingénieux. C’était avoir de l’instrumentation et un flux de travail prévisible avant la crise. « Ennuyeux » est ce que vous appelez la fiabilité quand elle fonctionne.

Points chauds fréquents de WordPress (ce qui est habituellement en faute)

1) wp-login.php et credential stuffing

CPU élevé plus beaucoup de POSTs vers wp-login.php est presque toujours un brute force ou credential stuffing. Même les logins échoués coûtent du CPU : hachage de mot de passe, gestion de session, et hooks de plugin qui s’exécutent sur les tentatives d’auth.

Que faire : appliquer des rate limits, ajouter des challenges anti-bot au bord, désactiver l’énumération d’utilisateurs, et envisager de mettre l’admin derrière un VPN ou une allowlist IP si l’organisation peut tolérer cela.

2) xmlrpc.php (pingback + bruteforce)

XML-RPC peut être abusé à la fois pour des tentatives de login et pour l’amplification pingback. Beaucoup de sites n’en ont pas besoin. Le désactiver réduit souvent l’empreinte d’attaque de façon significative.

3) admin-ajax.php (polling et fragments)

Admin AJAX est la source n°1 de CPU côté « plugin l’a fait ». Des scripts front pollent pour des mises à jour (widgets chat, constructeurs de pages, analytics), WooCommerce rafraîchit des fragments, et certains plugins utilisent admin-ajax comme API parce que c’est disponible.

Signe : taux de requêtes énorme, endpoint constant, nombreuses IPs, et slowlog montre des fonctions de plugin.

4) Endpoints REST avec requêtes non bornées

Les endpoints REST peuvent être martelés par des scrapers ou par votre propre front si vous êtes headless. Si vous autorisez de grands per_page, des filtres coûteux, ou des meta queries profondes, vous avez construit un broyeur de CPU avec une belle interface.

5) Recherche et filtrage (LIKE, meta queries)

La recherche WordPress est notoirement coûteuse sur de grands jeux de données, surtout avec WooCommerce et les champs personnalisés. Les meta queries sur des tables meta non indexées sont une catastrophe lente.

6) Tempêtes wp-cron

Le cron déclenché par le trafic signifie que les pics peuvent provoquer plus d’exécutions de cron, ce qui consomme plus de CPU, ce qui ralentit le site, ce qui augmente le recouvrement. C’est une boucle de rétroaction avec un nom poli.

7) Plugins « de sécurité » qui scannent chaque requête

Certains plugins de sécurité inspectent profondément chaque requête (regex, vérifications de fichier, appels de réputation IP). Ils peuvent être utiles, mais ils peuvent aussi devenir votre plus gros consommateur CPU. Mesurez-les comme n’importe quel autre plugin.

Atténuations efficaces (et celles qui posent problème plus tard)

Bloquez et rate-limitez au bord en premier

Si du trafic hostile fait partie du problème, ne « réparez pas en PHP ». PHP est la mauvaise couche pour de l’abus volumétrique. Utilisez un WAF/CDN, le rate limiting Nginx, ou des règles firewall.

Exemples de rate limiting Nginx

Si vous contrôlez Nginx, vous pouvez limiter les endpoints abusifs. Le but n’est pas de punir les utilisateurs légitimes. C’est d’empêcher un acteur unique de consommer tous les workers.

cr0x@server:~$ sudo nginx -T | grep -n "limit_req_zone" | head
53:limit_req_zone $binary_remote_addr zone=wp_login:10m rate=5r/m;
54:limit_req_zone $binary_remote_addr zone=wp_ajax:10m rate=30r/m;

Sens : Définit des taux par IP. Le login doit être lent ; l’AJAX peut être plus élevé mais pas infini.

Décision : Appliquez-les aux bonnes locations, puis reload Nginx et surveillez les 429.

cr0x@server:~$ sudo grep -n "location = /wp-login.php" -n /etc/nginx/sites-enabled/default
121:location = /wp-login.php {
122:    limit_req zone=wp_login burst=10 nodelay;
123:    include snippets/fastcgi-php.conf;
124:    fastcgi_pass unix:/run/php/php8.2-fpm.sock;
125:}

Sens : Les tentatives de login sont throttlées. Les bursts permettent des pics courts ; l’abus soutenu reçoit des 429.

Décision : Si vous recevez des plaintes utilisateurs, augmentez légèrement le burst mais gardez le taux bas. Le brute force est patient.

Rendez wp-cron prévisible

Désactivez WP-Cron déclenché par le trafic et exécutez-le via un vrai cron. C’est une modification qui réduit les « pics mystère ».

cr0x@server:~$ sudo -u www-data wp config set DISABLE_WP_CRON true --raw
Success: Updated the constant 'DISABLE_WP_CRON' in the 'wp-config.php' file.

Sens : WordPress arrête de lancer cron lors des chargements de page.

Décision : Ajoutez une entrée crontab système pour exécuter wp-cron.php à un intervalle raisonnable.

cr0x@server:~$ sudo crontab -u www-data -l | tail -n 3
*/5 * * * * cd /var/www/html && wp cron event run --due-now >/dev/null 2>&1

Sens : Le cron est maintenant piloté dans le temps, pas par le trafic.

Décision : Si vous avez beaucoup de tâches background (actions WooCommerce), ajustez l’intervalle ou gérez les runners Action Scheduler plus finement.

Utilisez le cache sérieusement (mais ne prétendez pas qu’il couvre tout)

Le cache full-page est excellent pour le contenu anonyme. Le cache d’objets est excellent pour les lectures BD répétées. Aucun des deux ne rendra le checkout WooCommerce bon marché. Ce qu’ils font, c’est réduire le travail répété afin que votre CPU soit utilisé pour les actions réelles des utilisateurs, pas pour des requêtes répétées sur le même menu.

Méfiez-vous des plugins « cachez tout » qui ajoutent des couches sans observabilité. Si vous ne pouvez pas mesurer le taux de hit et les évictions, vous opérez sur des rumeurs.

Dimensionnez correctement PHP-FPM (ne le mettez pas en « infini »)

Les pics CPU commencent souvent par des requêtes lentes, puis deviennent de la mise en file. Si vous réglez pm.max_children trop bas, vous mettez en file. Trop haut, vous manquez de RAM et vous thrasherez. Il y a un point optimal, et il dépend de la mémoire moyenne par worker PHP.

cr0x@server:~$ ps --no-headers -o rss -C php-fpm8.2 | awk '{sum+=$1; n++} END{print "avg_rss_kb=" int(sum/n) ", workers=" n}'
avg_rss_kb=89234, workers=24

Sens : RSS moyen d’un worker ~87MB. Multipliez par max children et ajoutez la surcharge pour MySQL, le cache OS et Nginx.

Décision : Si vous avez 2GB de RAM, 24 workers à 87MB font déjà ~2.1GB, ce qui n’est pas possible sans swap. Limitez-le, puis réduisez la mémoire par requête.

Erreurs courantes : symptôme → cause racine → correctif

Cette section vous aide à reconnaître votre propre incident. C’est normal. On est tous passés par là. La différence est de l’écrire et d’arrêter de répéter les mêmes erreurs.

1) Symptom: CPU 100%, « load average » énorme ; site timeout

Cause racine : la file d’écoute PHP-FPM augmente parce que les requêtes sont lentes, pas parce que vous avez besoin de plus de workers.

Fix : Activez le slowlog PHP-FPM et attribuez les chemins lents ; bloquez/rate-limitez les endpoints abusifs ; réduisez le travail par requête ; puis ajustez max children en fonction de la RAM.

2) Symptom: CPU élevé après activation d’un plugin de cache

Cause racine : préchauffage du cache ou préchargement agressif touchant chaque URL ; ou des misses de cache provoquant des stampedes sous concurrence.

Fix : Désactivez le warmup agressif en production ; ajoutez du locking si le plugin le supporte ; réduisez la concurrence des warmers ; assurez-vous que le cache d’objets est bien configuré.

3) Symptom: pics toutes les quelques minutes comme une horloge

Cause racine : WP-Cron ou Action Scheduler exécutant des tâches lourdes, déclenchés par le trafic ou une mauvaise configuration de planning.

Fix : Désactivez WP-Cron et exécutez un vrai cron ; inspectez les événements planifiés ; corrigez les tâches qui font trop à chaque run.

4) Symptom: beaucoup de requêtes admin-ajax ; CPU fond sous un trafic « normal »

Cause racine : rafraîchissements de fragments WooCommerce, assets d’éditeur de page, ou polling front fréquent (heartbeat, chat) générant des appels non mis en cache à haute fréquence.

Fix : Réduisez la fréquence ; désactivez les fragments quand c’est sûr ; mettez en cache les réponses ; assurez-vous que ces endpoints ne sont pas appelés pour les utilisateurs anonymes sans nécessité.

5) Symptom: MySQL CPU élevé ; PHP modéré ; requêtes montrent « Sending data »

Cause racine : requêtes non indexées (meta queries, LIKE searches), tables volumineuses, ou tris coûteux.

Fix : activez le slow query log ; identifiez la source des requêtes ; ajoutez des index avec précaution ; changez les réglages plugin pour éviter les pires requêtes ; ajoutez du cache d’objets.

6) Symptom: CPU élevé seulement pendant les crawls ; pages majoritairement cacheables

Cause racine : bots contournant le cache via query strings, headers, ou frappant des endpoints non mis en cache comme sitemaps et feeds.

Fix : mettez en cache les sitemaps ; ajoutez des règles de cache pour les patterns bots courants ; rate-limitez ; bloquez les user-agents abusifs qui ignorent robots.txt.

7) Symptom: 502/504 aléatoires quand le CPU grimpe

Cause racine : timeouts upstream (Nginx/Apache attendant PHP-FPM), workers insuffisants, ou workers coincés sur des appels BD/distants lents.

Fix : corrélez les logs d’erreur avec le slowlog PHP-FPM ; augmentez les timeouts seulement après avoir réduit la durée des requêtes ; évitez de masquer un backend lent en augmentant indéfiniment les timeouts.

Listes de contrôle / plan étape par étape

Étape par étape : trouver le bot ou plugin qui martèle en 30–60 minutes

  1. Vérifier que le CPU est réel : contrôlez mpstat pour steal et iowait.
  2. Identifier le process le plus chaud : ps trié par CPU. Généralement PHP-FPM.
  3. Vérifier la file PHP : page de statut FPM ; confirmez la croissance de la file.
  4. Trouver les endpoints en tête : parsez les logs d’accès pour les chemins URL les plus fréquents.
  5. Trouver les clients en tête : top IPs pour les endpoints chauds. Décidez de bloquer/rate-limiter maintenant.
  6. Activer le slowlog PHP-FPM (fenêtre courte). Capturez des backtraces.
  7. Mapper le slowlog au plugin/thème : chemins de fichier sous wp-content/plugins ou dossiers de thème.
  8. Confirmer avec un test contrôlé : désactivez temporairement le plugin via WP-CLI ou feature flag ; surveillez CPU et latence.
  9. Si la BD est suspecte : vérifiez processlist et slow query log ; mappez les requêtes aux endpoints.
  10. Appliquer une correction durable : remplacement/config plugin, cache, rate limits, changement de cron, optimisation de requêtes.
  11. Rédiger une petite note runbook : quel endpoint, quel plugin, quelle mitigation, quelle métrique confirme la santé.

Checklist : atténuations sûres à appliquer pendant un incident

  • Rate-limiter wp-login.php, xmlrpc.php et les routes /wp-json/ abusives.
  • Bloquer les IPs manifestement malveillantes (préférez les rate limits aux blocages whack-a-mole).
  • Activer temporairement le slowlog PHP-FPM ; collecter des preuves.
  • Scale vertical seulement si le steal time est faible et que vous avez la preuve d’une limitation CPU (pas I/O ou BD bloquée).
  • Limiter les workers PHP-FPM pour éviter la mort par swap.
  • Désactiver WP-Cron et exécuter un vrai cron.
  • Désactiver le plugin spécifique identifié par les traces slowlog si le site peut le tolérer.
  • Désactiver les fonctionnalités non essentielles et coûteuses (produits liés, recherche en direct, filtres fancy) pour la durée de l’incident.

Checklist : changements à planifier après l’incident

  • Conserver une baseline : taux de requêtes, p95 de latence, file PHP-FPM, endpoints en tête.
  • Implémenter des logs d’accès structurés incluant le temps upstream.
  • Garder le slow query log disponible (même si normalement off) et savoir l’activer rapidement.
  • Établir un « budget de performance des plugins » : éviter les plugins qui font un gros travail à chaque requête.
  • Cadence de mises à jour avec staging et plan de rollback.

FAQ

1) Comment savoir si ce sont des bots ou de vrais utilisateurs ?

Recherchez une concentration par IP, user agent et comportement. Les bots frappent souvent le même endpoint à haute fréquence, ignorent les cookies, et montrent peu de diversité de pages. Les vrais utilisateurs naviguent.

2) Dois-je simplement ajouter plus de cœurs CPU ?

Seulement après avoir attribué le travail. Le scaling peut gagner du temps, mais augmente aussi la facture et peut amplifier les goulots BD. Si un endpoint est abusif, bloquez-le d’abord.

3) admin-ajax.php est-il toujours mauvais ?

Non. C’est un mécanisme légitime. Il devient mauvais lorsqu’il est utilisé comme API haute fréquence pour des utilisateurs anonymes ou quand les réponses déclenchent des requêtes BD coûteuses.

4) Quelle est la manière la plus rapide pour trouver le plugin qui cause la charge ?

Les backtraces du slowlog PHP-FPM sont la méthode fiable la plus rapide. Les logs d’accès vous disent ce qui est chaud ; le slowlog vous dit quel code est lent.

5) Mon CPU est élevé mais PHP-FPM ne l’est pas. Que faire ?

Vérifiez le CPU MySQL et la processlist. Contrôlez aussi l’attente I/O et l’usage du swap. Parfois c’est l’indexation de recherche, le traitement d’images, des jobs de backup, ou même un autre locataire sur la même machine.

6) Le cache peut-il corriger les pics CPU WooCommerce ?

Partiellement. Vous pouvez mettre en cache les pages anonymes et certains fragments, mais panier/checkout et le comportement connecté restent dynamiques. Concentrez-vous sur la réduction du chatter admin-ajax et des requêtes coûteuses.

7) Dois-je désactiver xmlrpc.php ?

Si vous ne l’utilisez pas (la plupart des sites ne l’utilisent pas), oui—désactivez-le ou bloquez-le. Si vous devez le conserver, appliquez des rate limits strictes et surveillez les tentatives de login.

8) Le cache d’objets Redis est-il toujours une bonne idée ?

C’est avantageux quand il réduit les lectures BD répétées et que le cache est stable. Il peut se retourner contre vous s’il masque du code inefficace jusqu’à une tempête d’évictions ou si une mauvaise configuration provoque des misses constantes.

9) Pourquoi les pics CPU sont-ils corrélés au cron ?

Parce que WP-Cron peut se déclencher lors des chargements de page, ce qui crée des boucles de rétroaction sous trafic. Le passer à un vrai cron transforme des pics surprises en travail planifié.

10) Quelle est la cause la plus courante d’un CPU à 100 % sur WordPress ?

Un volume élevé de requêtes vers un endpoint dynamique qui contourne le cache, combiné à du code plugin lent ou des requêtes BD lentes. Rarement le seul core WordPress en est responsable.

Conclusion : prochaines étapes à faire aujourd’hui

Si votre site WordPress atteint la limite CPU, ne le traitez pas comme un mystère. Traitez-le comme une enquête fondée sur des preuves.

  1. Exécutez le playbook de diagnostic rapide : identifiez process → endpoint → client → chemin de code.
  2. Activez le slowlog PHP-FPM pour une courte fenêtre et capturez des backtraces pendant le pic.
  3. Parsez les logs d’accès pour trouver les URL et IPs en tête. Rate-limitez les endpoints abusifs immédiatement.
  4. Mappez les traces de pile aux plugins et désactivez/remplacez les coupables via des tests contrôlés.
  5. Rendez le cron prévisible et dimensionnez PHP-FPM pour éviter la mise en file et le thrash de swap.
  6. Notez ce que vous avez appris : l’endpoint, le plugin, la mitigation, et la métrique qui prouve que c’est corrigé. Le vous du futur vous remerciera.

Le CPU n’est pas une faute morale. C’est une facture que vous payez en temps réel. Obtenez le reçu : la requête, le plugin, la requête SQL, le client. Puis faites en sorte que ça cesse.

← Précédent
ARC pour les e-mails expliqué (version courte et utile) — quand il aide le courrier transféré
Suivant →
Écrans squelettes en pur CSS : lueur, réduction du mouvement et performance

Laisser un commentaire