Avviso max_children di php-fpm: trova la causa nei log prima di aumentare i worker

L'avviso max_children di php-fpm è un segnale che tutti i processi worker di PHP sono occupati con richieste, quindi le nuove sono finite in coda. Su un server WordPress con Nginx, Apache e php-fpm dove questo avviso si accumulava da settimane, ho usato i log per individuare le richieste che riempivano i worker e l'ho risolto con un rate limit in nginx invece di alzare l'impostazione.
Nel controllo settimanale l'avviso veniva registrato 27–187 volte al giorno, eppure negli stessi quattro giorni i visitatori non hanno ricevuto nessun errore 502 o 504. A riempire i worker non erano i visitatori ma un bot che martellava gli URL di amministrazione e uno scanner che chiedeva percorsi inesistenti.
1️⃣ php-fpm max_children & seems busy
Quando l'avviso arriva a 66 al giorno, viene facile leggerlo come 66 volte in cui mancavano worker. Il log di php-fpm mescola due avvisi diversi, e la maggior parte sono preavvisi registrati prima di raggiungere il 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] significa che i worker inattivi sono 0 e si sta creando un nuovo processo; con la gestione dei processi su dynamic viene registrato anche per un breve aumento delle richieste. [server reached pm.max_children] è il momento in cui è stato raggiunto il limite di 26 e non se ne potevano creare altri, e da lì le nuove richieste aspettano.
È la differenza tra chiamare un cameriere in più e lasciare i clienti fuori perché tutti i posti sono occupati.
Contando un giorno per tipo sono risultati 58 seems busy e 6 max_children. La routine di controllo li ha sommati e ha segnalato 66, ma il limite è stato davvero raggiunto sei volte.
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:44I sei orari stampati dal secondo comando diventano il riferimento per rintracciare la causa; il passo successivo usa solo questi orari.
2️⃣ Perché alzare max_children non è la soluzione
Alzare il limite di 26 può ridurre il numero di avvisi, ma non cambia quali richieste hanno riempito i worker.
Il calcolo comune divide la memoria disponibile per la memoria media di un worker. Sul server misurato, 26 worker usavano in media 131MB e al massimo 214MB, e calcolando con la memoria disponibile rimasta, sulla carta c'era margine per crearne altri.
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 -mI quattro valori del primo output sono numero di worker, memoria media (MB), memoria massima (MB) e totale (MB); la colonna available dell'output di free è la memoria disponibile.
Quando i worker superano i core della CPU, altri worker non aumentano le richieste gestite insieme; allungano solo il tempo di ciascuna. Finché i visitatori ricevono zero errori 502 o 504, alzare il limite non porta nulla e dà solo più processi ai bot e agli scanner che occupavano i worker.
Prima di alzare il limite, controlla se le richieste che occupavano i worker negli orari degli avvisi erano visitatori o no; la sezione successiva spiega come verificarlo in access.log.
3️⃣ Rintracciare la causa in access.log: orario, IP, percorso, codice di stato
Scegli un orario di avviso e la risposta è nell'access.log di quel minuto o due. L'ordine è: estrarre le righe per orario, contare per IP e poi controllare percorsi e codici di stato degli IP in cima.
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 404Il 9 ottobre alle 18:09 sono state registrate 286 richieste in un minuto; un IP ne aveva fatte 115, i percorsi erano URL di amministrazione come /wp-admin/profile.php e tutti i codici di stato erano 302.
Il codice di stato dice se PHP è stato raggiunto. Un 200 per HTML in cache servito direttamente da nginx, e 444 o 400, non toccano mai PHP, mentre 302, 404 e un 200 non in cache usano ciascuno un worker. Quando qualcuno senza login richiede un URL di amministrazione, WordPress risponde con un 302 alla pagina di login, e quel singolo 302 è un'esecuzione di PHP.
Se un server ospita due o più siti, l'access.log globale registra le righe di entrambi. All'inizio pensavo che questo bot attaccasse il primo sito, ma il confronto con il log di ciascun sito ha mostrato che 22.390 delle 22.391 richieste erano per il secondo.
4️⃣ Tre tipi di richieste che hanno riempito i worker: wp-admin di un bot, 404 di uno scanner, preload della cache
Applicando lo stesso ordine a tutti gli orari degli avvisi, sono emersi tre tipi di richieste che riempivano i worker.
Il primo era un bot che richiedeva di continuo gli URL di amministrazione. Un IP ha inviato 22.391 richieste in tre ore; 9.944 erano per quattro percorsi tra cui /wp-admin/profile.php, e siccome ogni 302 senza login è un'esecuzione di PHP, ha usato un worker 9.944 volte.
Il secondo era uno scanner di vulnerabilità che chiedeva percorsi inesistenti. Anche con Googlebot nello User-Agent non è Googlebot: l'IP che ha inviato 14.672 richieste al giorno era un server virtuale di Google Cloud con DNS inverso che termina in googleusercontent.com, mentre Googlebot termina in googlebot.com. Lo stesso IP si alternava tra bingbot, Applebot, GPTBot e ClaudeBot.
Le richieste di file come .env vengono troncate da nginx con un 444 in 0,04 secondi, ma i percorsi comuni non coperti dalle regole di blocco, come /userfiles o /secrets.yml, fanno generare a WordPress una pagina 404. Quella pagina 404 costa 266KB e 1,4–2,9 secondi, quanto generare un articolo normale, e un IP l'ha richiesta 4.069 volte al giorno.
Il terzo era il server stesso. Salvare un articolo o una traduzione svuota la cache e il preload di WP Rocket richiede di nuovo tutti gli URL, per 8.489 richieste al giorno e fino a 60 al minuto. Anche i controlli dello stato di indicizzazione di Rank Math hanno usato un worker 754 volte al giorno tramite Action Scheduler. Entrambi si concentravano nelle ore di lavoro e non hanno causato errori ai visitatori, quindi li ho lasciati fuori dall'intervento.
Nel simulatore qui sotto vedi a quale livello termina ogni richiesta e se usa un worker PHP.
| Richiesta | Si ferma a | Risposta | Worker |
|---|---|---|---|
| wp-admin del bot | WordPress | 302 (alla pagina di login) | Ne usa 1 |
| Scanner tipo .env | Regola di blocco di nginx | 444 (0,04 s) | Non usato |
| Scanner percorso falso comune | WordPress | Pagina 404 (266KB · 1,4–2,9 s) | Ne usa 1 |
| Preload della cache | WordPress | 200 · salvato come file di cache | Ne usa 1 |
| Visita a un articolo in cache | File di cache | 200 (nginx risponde direttamente) | Non usato |
| Visita a un articolo non in cache | WordPress | 200 · salvato come file di cache | Ne usa 1 |
5️⃣ Rate limiting in nginx: limitare le richieste senza login e ripristinare l'IP reale
Ho scelto il rate limiting invece del blocco. Blocchi un percorso e lo scanner cambia percorso; blocchi un IP e il giorno dopo ne arriva un altro. Sono limitate solo le richieste senza login che arrivano a PHP, a 30 al minuto e 60 contemporanee per IP, e l'eccedenza riceve un 429.
Per un server dietro una CDN questa impostazione da sola non basta. L'IP che vede nginx è l'indirizzo dell'edge della CDN, quindi tutti i visitatori che passano da quell'edge vengono raggruppati in un solo IP; prima devi registrare l'elenco degli IP degli edge con set_real_ip_from per ripristinare l'IP reale da X-Forwarded-For. Le richieste con cookie di login sono esenti, ma nulla viene esentato in base allo User-Agent.
# contesto http
real_ip_header X-Forwarded-For;
real_ip_recursive on;
set_real_ip_from 203.0.113.0/24; # elenco degli edge della 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 che passa a PHP
location @apache {
limit_req zone=php_anon burst=60 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:8080;
}Le pagine in cache ricevono risposta dentro nginx da try_files e non arrivano mai a questa location, e admin-ajax.php è una location separata, quindi non rientra nel limite.
Prova con richieste simultanee. Le richieste in sequenza non superano mai 30 al minuto perché un 404 richiede 1,5 secondi, quindi non toccano mai il limite; inviando 100 richieste, 20 alla volta, si ottengono 67 risposte 404 e 33 risposte 429, a conferma che il limite funziona.
seq 1 100 | xargs -P 20 -I{} curl -s -o /dev/null -w '%{http_code}\n' https://example.com/zz-{} | sort | uniq -cSe l'output conta dei 429, il limite funziona. Se hai esentato l'IP del server stesso, togli l'esenzione per un attimo durante il test o eseguilo da un'altra rete.
6️⃣ Prima e dopo
Il limite sugli URL di amministrazione senza login è entrato in funzione la sera del 9 ottobre, e quello sulle richieste che arrivano a PHP la sera dell'11. I 302 degli URL di amministrazione sono scesi da 9.996 il 9 a 11 il 10 e 1 l'11, e l'IP che aveva inviato 22.391 richieste in tre ore non ne ha più inviate dal giorno dopo.
Per lo scanner il limite è appena stato applicato, quindi non ci sono ancora numeri di un giorno intero; il giudizio verrà da quanto scenderà, nel controllo della prossima settimana, il numero di pagine 404 generate da WordPress (2.563 il 9, 2.612 il 10, 1.630 l'11). Gli errori 502 e 504 dei visitatori erano 0 sia prima sia dopo.
Questi numeri sono stati contati direttamente dai log di un server WordPress con Nginx, Apache e php-fpm, e il giudizio non si è basato sul numero di avvisi ma sugli errori dei visitatori e sulle richieste che arrivano a PHP.
🔢 FAQ & contenuti consigliati
Cache
Cache del sito web & cache interna del server
Spiega livello per livello come le richieste in cache evitano PHP.
Sicurezza
Vulnerabilità di wp-config.php
Come proteggere con permessi e regole del server il file di configurazione preso di mira dagli scanner.
Linux
15 categorie di comandi Linux
Una tabella che divide i comandi in 15 categorie, compresi grep e awk usati per contare i log.
ℹ️ Informativa sull'affiliazione
I contenuti di questo sito includono link di affiliazione. Se un visitatore acquista un prodotto o un servizio tramite uno di questi link, il sito riceve una commissione dal venditore. In questo processo l'importo pagato dall'acquirente (durante le promozioni l'importo scende↓) non aumenta. Prezzi, sconti e disponibilità indicati si riferiscono al momento della stesura e possono differire da quelli reali: prima di acquistare verifica sempre presso il venditore. I prodotti vengono scelti e valutati secondo criteri propri, e le commissioni non influiscono sull'ordine di presentazione né sul contenuto delle valutazioni.