Avertissement php-fpm max_children : trouver la cause dans les journaux avant d'ajouter des workers

L'avertissement php-fpm max_children signale que tous les processus workers PHP traitent des requêtes et que les nouvelles requêtes passent en file d'attente. Sur un serveur WordPress Nginx + Apache avec php-fpm où cet avertissement s'accumulait depuis des semaines, j'ai isolé dans les journaux les requêtes qui remplissaient les workers : au lieu de relever la valeur du réglage, la solution a été une limitation de débit dans nginx.
Lors du contrôle hebdomadaire, l'avertissement était enregistré 27 à 187 fois par jour, mais sur la même période, les visiteurs n'ont reçu aucune erreur 502 ou 504 en quatre jours : ce qui remplissait les workers, ce n'étaient pas les visiteurs, mais un robot qui demandait sans cesse les adresses d'administration et un scanner qui demandait des chemins inexistants.
1️⃣ php-fpm max_children & seems busy
Quand l'avertissement s'accumule 66 fois par jour, on lit facilement qu'il a manqué des workers 66 fois. Le journal de php-fpm mêle deux avertissements de nature différente, et la plupart sont des avertissements préalables, enregistrés avant d'atteindre la limite.
[09-Oct-2026 00:16:18] WARNING: [pool www] seems busy (you may need to increase pm.start_servers, or pm.min/max_spare_servers), spawning 8 children, there are 0 idle, and 15 total children
[09-Oct-2026 13:02:05] WARNING: [pool www] server reached pm.max_children setting (26), consider raising it[seems busy] indique qu'il ne reste plus de worker en attente et qu'un nouveau processus est en cours de création ; avec une gestion des processus en dynamic, il apparaît au moindre pic de requêtes. [server reached pm.max_children] marque le moment où la limite de 26 est atteinte et où aucun processus de plus ne peut être créé : à partir de là, les nouvelles requêtes attendent.
C'est la différence, au restaurant, entre appeler un serveur de plus et faire attendre dehors des clients parce que toutes les tables sont prises.
En les comptant par type sur une journée, on obtient 58 seems busy et 6 max_children. La routine de contrôle a additionné les deux avertissements pour signaler 66 occurrences, mais la limite n'a réellement été atteinte que six fois.
grep '09-Oct-2026' /var/log/php-fpm/error.log | grep 'pool www' | grep -c 'seems busy'58grep '09-Oct-2026' /var/log/php-fpm/error.log | grep 'max_children' | cut -c2-2109-Oct-2026 13:02:05
09-Oct-2026 13:52:10
09-Oct-2026 18:09:20
09-Oct-2026 18:42:17
09-Oct-2026 18:46:07
09-Oct-2026 19:12:44Les six horaires affichés par la deuxième commande servent de point de départ pour remonter à la cause ; l'étape suivante n'utilise que ces horaires.
2️⃣ Pourquoi augmenter max_children n'est pas la réponse
Relever la limite de 26 peut réduire le nombre d'avertissements, mais cela ne change rien à la nature des requêtes qui remplissent les workers.
Le calcul habituel consiste à diviser la mémoire disponible par la mémoire moyenne d'un worker. Sur le serveur mesuré, les 26 workers utilisaient en moyenne 131 Mo, au maximum 214 Mo, soit 3,4 Go au total, et 4,1 Go des 15,7 Go de mémoire étaient disponibles : en chiffres, on pourrait en créer plus de 30 de plus.
ps -o rss= -u www -C php-fpm | awk '{s+=$1; n++; if($1>m) m=$1} END{print n, s/n/1024, m/1024, s/1024}'26 130.579 214.27 3395.05free -m total used free shared buff/cache available
Mem: 15783 11658 397 340 4383 4124
Swap: 8191 1615 6576nproc4Les quatre valeurs de la première sortie sont le nombre de workers, la mémoire moyenne (Mo), la mémoire maximale (Mo) et le total (Mo) ; la colonne available de la sortie de free est la mémoire disponible.
Avec un CPU de 4 cœurs, ajouter des workers n'augmente pas le nombre de requêtes traitées en même temps : seul le temps de chaque requête s'allonge. Tant que les visiteurs ne reçoivent aucune erreur 502 ou 504, relever la limite ne rapporte rien et donne plus de processus aux robots et aux scanners qui occupaient les workers.
Avant de relever la limite, il faut vérifier si les requêtes qui occupaient les workers aux heures des avertissements venaient de visiteurs ou non ; la méthode de vérification dans access.log est détaillée dans la section suivante.
3️⃣ Remonter à la cause dans access.log : heure, IP, chemin, code de réponse
Choisissez l'heure d'un avertissement : la réponse se trouve dans l'access.log de ces 1 ou 2 minutes. On extrait d'abord les lignes par l'heure, on les compte par IP, puis on regarde les chemins et les codes de réponse des IP en tête.
grep '09/Oct/2026:18:09' /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head 115 140.245.x.x
61 187.15.x.x
17 3.36.x.x
8 91.239.x.x
4 79.127.x.x
3 109.248.x.x
3 109.248.x.x
3 109.248.x.x
2 79.127.x.x
2 40.77.x.xgrep '09/Oct/2026:18:09' /var/log/nginx/access.log | awk '{print $7, $9}' | sort | uniq -c | sort -rn | head 55 / 200
13 /wp-login.php 404
13 /wp-admin/plugins.php 302
13 /wp-admin/index.php 302
13 /wp-admin/edit.php 302
12 /wp-admin/profile.php 302
4 / 499
3 /xmlrpc.php 302
2 /xmlrpc.php 499
2 /wp-content/themes/kinsta/images/duos/small/wheelchair.svg 404Le 9 octobre à 18 h 09, 286 requêtes ont été enregistrées en une minute, dont 115 d'une seule IP ; les chemins étaient des adresses d'administration comme /wp-admin/profile.php, et les codes de réponse tous des 302.
Le code de réponse indique si la requête est arrivée jusqu'à PHP. Un 200 pour du HTML en cache servi directement par nginx, ainsi que les 444 et 400, ne passent pas par PHP, tandis que les 302, les 404 et les 200 non mis en cache occupent un worker. Quand on demande une adresse d'administration sans être connecté, WordPress répond par un 302 vers la page de connexion, et chacun de ces 302 correspond à une exécution de PHP.
Quand un serveur héberge plusieurs sites, l'access.log global contient les lignes de tous ces sites. J'ai d'abord cru que ce robot attaquait le premier site, mais en comparant avec les journaux par site, 22 390 des 22 391 requêtes concernaient le deuxième site.
4️⃣ Trois types de requêtes remplissaient les workers : le wp-admin d'un robot, les 404 d'un scanner, le préchargement du cache
En appliquant la même méthode à toutes les heures d'avertissement, les requêtes qui remplissaient les workers étaient de trois types.
Le premier était un robot qui demandait sans cesse les adresses d'administration. Une IP a envoyé 22 391 requêtes en trois heures, dont 9 944 sur quatre chemins, parmi lesquels /wp-admin/profile.php ; comme chaque 302 sans connexion correspond à une exécution de PHP, cela a occupé un worker 9 944 fois.
Le deuxième était un scanner de vulnérabilités qui demandait des chemins inexistants. Même si le User-Agent indique Googlebot, ce n'est pas Googlebot : l'IP qui a envoyé 14 672 requêtes en une journée était un serveur virtuel Google Cloud dont le DNS inverse se terminait par googleusercontent.com, alors que celui de Googlebot se termine par googlebot.com. La même IP alternait entre bingbot, Applebot, GPTBot et ClaudeBot.
Les requêtes de fichiers comme .env sont bloquées par nginx en 444 en 0,04 seconde, mais pour des chemins ordinaires absents des règles de blocage, comme /userfiles ou /secrets.yml, WordPress génère une page 404. Cette page 404 pèse 266 Ko et prend 1,4 à 2,9 secondes, le même coût que la génération d'un article normal, et une seule IP en a demandé 4 069 par jour.
Le troisième était le serveur lui-même. Quand on enregistre un article ou une traduction, le cache est vidé et le préchargement de WP Rocket redemande toutes les adresses : 8 489 requêtes par jour, jusqu'à 60 par minute. La vérification de l'état d'indexation de Rank Math occupait aussi un worker 754 fois par jour via Action Scheduler. Ces deux tâches se concentrent sur les heures de travail et ne provoquent pas d'erreurs côté visiteurs : je les ai donc exclues des mesures.
Dans le simulateur ci-dessous, vous voyez à quelle couche chaque requête s'arrête et si elle occupe un worker PHP.
| Requête | Couche d'arrêt | Réponse | Workers |
|---|---|---|---|
| wp-admin d'un robot | WordPress | 302 (vers la page de connexion) | 1 occupé |
| Scanner, fichiers .env | Règle de blocage nginx | 444 (0.04 s) | Non occupé |
| Scanner, faux chemin ordinaire | WordPress | Page 404 (266 Ko · 1.4–2.9 s) | 1 occupé |
| Préchargement du cache | WordPress | 200 · enregistré en fichier de cache | 1 occupé |
| Visite d'un article en cache | Fichier de cache | 200 (nginx répond directement) | Non occupé |
| Visite d'un article non mis en cache | WordPress | 200 · enregistré en fichier de cache | 1 occupé |
5️⃣ Limitation de débit nginx : limiter les requêtes sans connexion et rétablir la vraie IP
J'ai choisi la limitation de débit plutôt que le blocage : si l'on bloque un chemin, le scanner en change, et si l'on bloque une IP, une autre se connecte le lendemain. Seules les requêtes sans connexion qui atteignent PHP sont limitées à 30 par minute et par IP, avec des pointes de 60, et l'excédent reçoit une réponse 429.
Pour un serveur derrière un CDN, ce réglage seul ne convient pas : l'IP que voit nginx est l'adresse d'un nœud edge du CDN, si bien que tous les visiteurs passant par ce nœud seraient regroupés sous une seule IP. Il faut d'abord enregistrer la liste des IP edge avec set_real_ip_from pour rétablir la vraie IP de X-Forwarded-For. Les requêtes avec un cookie de connexion sont exemptées, mais pas sur la base du User-Agent.
# contexte http
real_ip_header X-Forwarded-For;
real_ip_recursive on;
set_real_ip_from 203.0.113.0/24; # liste des nœuds edge du CDN
map $http_cookie $php_anon_key {
default $binary_remote_addr;
~*wordpress_logged_in '';
}
limit_req_zone $php_anon_key zone=php_anon:10m rate=30r/m;
# location qui transmet à PHP
location @apache {
limit_req zone=php_anon burst=60 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:8080;
}Les pages en cache sont servies dans nginx par try_files et n'arrivent donc jamais à cette location, et admin-ajax.php, qui a sa propre location, n'est pas concerné par la limite.
On vérifie avec des requêtes simultanées. En séquence, chaque 404 prend 1,5 seconde : on ne dépasse jamais 30 requêtes par minute et la limite ne se déclenche pas. En envoyant 100 requêtes, 20 à la fois, on obtient 67 réponses 404 et 33 réponses 429, ce qui confirme que la limite fonctionne.
seq 1 100 | xargs -P 20 -I{} curl -s -o /dev/null -w '%{http_code}\n' https://example.com/zz-{} | sort | uniq -cSi la sortie compte des 429, la limite fonctionne ; si vous avez exempté l'IP du serveur lui-même, levez temporairement l'exemption pendant le test ou lancez-le depuis un autre réseau.
6️⃣ Avant et après
La limite sur les adresses d'administration sans connexion a été appliquée le soir du 9 octobre, celle sur les requêtes qui atteignent PHP le soir du 11. Les 302 sur les adresses d'administration sont passés de 9 996 le 9 à 11 le 10, puis à 1 le 11, et l'IP qui avait envoyé 22 391 requêtes en trois heures n'en a plus envoyé aucune dès le lendemain.
Côté scanner, la limite venait d'être appliquée et il n'y a pas encore de chiffres sur une journée complète : on jugera au contrôle de la semaine suivante, d'après la baisse du nombre de pages 404 générées par WordPress (2 563 le 9, 2 612 le 10, 1 630 le 11). Les erreurs 502 et 504 côté visiteurs sont restées à 0 avant comme après.
Ces chiffres ont été comptés directement dans les journaux d'un serveur WordPress tournant avec Nginx, Apache et php-fpm, et j'ai jugé non pas sur le nombre d'avertissements, mais sur les erreurs des visiteurs et le nombre de requêtes atteignant PHP.
🔢 FAQ & contenus recommandés
Cache
Cache de site web & cache côté serveur
Explique couche par couche pourquoi les requêtes en cache ne passent pas par PHP.
Sécurité
Failles de sécurité de wp-config.php
Comment protéger le fichier de configuration visé par les scanners avec les droits et les règles du serveur.
Linux
Les 15 catégories de commandes Linux
Un tableau qui classe les commandes en 15 catégories, dont grep et awk, utilisés pour décompter les journaux.
ℹ️ Mention d'affiliation
Le contenu de ce site contient des liens d'affiliation. Lorsqu'un visiteur achète un produit ou un service via l'un de ces liens, le site perçoit une commission du vendeur. Le montant payé par l'acheteur(il baisse lors des promotions ↓), lui, n'augmente pas. Les prix, remises et stocks indiqués correspondent à la date de rédaction et peuvent différer ; vérifiez-les donc auprès du vendeur avant d'acheter. Les produits sont choisis et évalués selon nos propres critères, et les commissions n'influencent ni l'ordre de présentation ni le contenu des évaluations.