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

Informativa sull'affiliazione v3.1
Informativa sull'affiliazione v3.2 (solo mobile)
Ambiente di test Famiglia RHELAlmaLinux 9.8account rootVerificato il 12/10/2026

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'
58
grep '09-Oct-2026' /var/log/php-fpm/error.log | grep 'max_children' | cut -c2-21
09-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:44

I 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.05
free -m

I 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.x
grep '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 404

Il 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.

🧭 Simulatore del percorso delle richiestePagina di amministrazione wp-adminSimulatore esplicativo
🗂️ Richieste che hanno riempito i worker
Richieste wp-admin di un bot
Quando qualcuno senza login richiede un URL di amministrazione, WordPress risponde con un 302 alla pagina di login. Quel singolo 302 è un'esecuzione di PHP.
Scanner: file tipo .env
Le richieste di file come .env vengono troncate dalla regola di blocco di nginx con un 444. 0,04 secondi, e PHP non viene toccato.
Scanner: percorsi falsi comuni
I percorsi comuni non coperti dalle regole di blocco, come /userfiles o /secrets.yml, fanno generare a WordPress una pagina 404. 266KB e 1,4–2,9 secondi, lo stesso costo di un articolo normale.
Preload della cache
Salvare un articolo o una traduzione svuota la cache e il preload di WP Rocket richiede di nuovo tutti gli URL. Fino a 60 al minuto, ogni richiesta usa un worker.
Visita a un articolo in cache
Se c'è un file di cache, nginx risponde direttamente e PHP non viene toccato.
Visita a un articolo non in cache
Se non c'è un file di cache, PHP genera la pagina e salva quell'HTML come file di cache.
Flusso completo
La richiesta scende dall'alto in basso e si ferma al livello in cui finisce la risposta. Usano un worker PHP solo le richieste che arrivano fino a WordPress.
RichiestaSi ferma aRispostaWorker
wp-admin del botWordPress302 (alla pagina di login)Ne usa 1
Scanner tipo .envRegola di blocco di nginx444 (0,04 s)Non usato
Scanner percorso falso comuneWordPressPagina 404 (266KB · 1,4–2,9 s)Ne usa 1
Preload della cacheWordPress200 · salvato come file di cacheNe usa 1
Visita a un articolo in cacheFile di cache200 (nginx risponde direttamente)Non usato
Visita a un articolo non in cacheWordPress200 · salvato come file di cacheNe usa 1
Un simulatore esplicativo che gira solo nel tuo browser. Risposte e tempi vengono da log e misure di un server con Nginx + Apache a ottobre 2026.

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 -c

Se 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.

📊Prima e dopo il rate limitAl giorno · conteggio dei log
PrimaDopo
Risposte 302 degli URL di amministrazione
302 inviati da WordPress alle richieste wp-admin senza login. Il limite sugli URL di amministrazione senza login è entrato in funzione la sera del 9 ottobre.
02,4994,9987,4979,996
10/99,996Prima
10/1011Dopo
10/111Dopo
Richieste dall'IP del bot
Richieste giornaliere dell'unico IP che ne ha inviate 22.391 in tre ore.
05,59811,19616,79322,391
10/922,391Prima
10/100Nessuna richiesta
10/110Nessuna richiesta
Pagine 404 generate da WordPress
Richieste dello scanner a percorsi falsi comuni. Il limite sulle richieste che arrivano a PHP è entrato in funzione la sera dell'11 ottobre, quindi si valuterà al controllo della prossima settimana.
06531,3061,9592,612
10/92,563Prima
10/102,612Prima
10/111,630Prima · applicato la sera dell'11
I numeri sono conteggi giornalieri dell'access.log di nginx dal 9 all'11 ottobre 2026.

🔢 FAQ & contenuti consigliati

dynamic crea e ferma i worker in base al volume di richieste e registra l'avviso seems busy quando i worker inattivi arrivano a 0. static mantiene sempre max_children worker, quindi non ci sono ritardi di creazione né avvisi, ma la memoria resta sempre occupata. Questo server è rimasto su dynamic con 26; se hai memoria in avanzo e il volume di richieste è costante tutto il giorno, va bene static.

Attivalo quando negli orari degli avvisi certe richieste sembrano durare a lungo. Imposta nella configurazione del pool il percorso [slowlog] e [request_slowlog_timeout], e per ogni richiesta che supera quel tempo viene registrato il punto di esecuzione. In questo caso la causa era il numero di richieste, quindi è bastato contare access.log e slowlog non è stato usato.

Il limite vale solo per le richieste senza login che aggirano la cache e arrivano a PHP, e solo l'eccedenza riceve un 429 quando un IP supera 30 al minuto o 60 contemporanee. Nel conteggio dell'11 ottobre l'unico IP sopra quella soglia era uno scanner, e lo stato di eccedenza si azzera entro un minuto, quindi la richiesta successiva riceve una risposta normale.

Non l'ho fatto. Il preload si concentrava negli orari in cui si salvavano articoli e traduzioni e non ha mai causato errori ai visitatori. Il criterio non è il numero di avvisi ma se i visitatori hanno ricevuto errori 502 o 504; se quel numero è 0, il preload resta com'è.

ℹ️ 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.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Dimostra di essere umano: 1   +   1   =