Résoudre l'erreur nginx 504 Gateway Time-out : trouver la cause au lieu de relever le délai

Publié le 21 min de lecture

Avec un 504, le backend était joignable, il a simplement répondu trop lentement. Comment déterminer avec le journal des temps où le temps se consomme, lequel des nombreux délais s'applique vraiment et pourquoi un délai plus long ne fait le plus souvent que repousser la panne.

Une erreur 504 Gateway Time-out est la plus patiente de toutes les pages d'erreur. nginx a accepté la requête, l'a transmise au backend, puis il a attendu jusqu'à l'expiration d'un délai fixé en interne. Le backend est resté joignable pendant tout ce temps, il n'a simplement pas répondu à temps. C'est exactement ce qui distingue ce code de son voisin : avec le 502 Bad Gateway, le backend répond mal ou pas du tout, avec le 504, il répond trop lentement. Ce guide montre comment mesurer où le temps se consomme réellement, laquelle des nombreuses limites de temps s'applique vraiment, et pourquoi relever cette limite est presque toujours la moins bonne des réponses disponibles.

Toutes les indications valent pour Debian 13 (trixie), Debian 12 (bookworm), Ubuntu 24.04 LTS et Ubuntu 22.04 LTS. Les commandes sont écrites pour une exécution en tant que root ; en utilisateur normal, placez sudo devant. Les exemples utilisent PHP 8.4, remplacez le numéro de version par celui de votre système :

SystèmePHPServiceConfiguration
Debian 13 (trixie)8.4php8.4-fpm/etc/php/8.4/fpm/
Debian 12 (bookworm)8.2php8.2-fpm/etc/php/8.2/fpm/
Ubuntu 24.04 LTS8.3php8.3-fpm/etc/php/8.3/fpm/
Ubuntu 22.04 LTS8.1php8.1-fpm/etc/php/8.1/fpm/
ls /etc/php/

Les directives nginx portent les mêmes noms sur les quatre systèmes. Les différences se situent du côté de PHP et du côté de la base de données, et elles sont signalées aux endroits concernés.

Savoir qui a abandonné indique où chercher

Avant d'ouvrir le moindre fichier, répondez à une question : quelle couche a abandonné ? Le code de statut le dit déjà.

CodeCe qui s'est passéOù chercher
500 Internal Server ErrorLe backend a répondu, sa réponse était une erreurJournal de l'application
502 Bad GatewayLa connexion n'a pas pu être établie ou elle a été coupéeService, socket, droits, plantages
504 Gateway Time-outLa connexion tenait, la réponse n'est pas arrivée dans le délaiTemps d'exécution dans le backend
408 Request TimeoutLe visiteur n'a pas fini d'envoyer sa propre requête à tempsEnvois de fichiers, raccordements lents
499 (seulement dans le journal)Le visiteur a abandonné avant que nginx ait terminéTrop lent, mais sous le délai

La ligne du 499 est la plus sous-estimée. Ce n'est pas une erreur, c'est un système d'alerte précoce : le visiteur a fermé l'onglet parce que la page mettait trop de temps. Si un 504 disparaît après que vous avez relevé le délai et que des 499 apparaissent à la place, rien n'est résolu.

awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head

Le champ 9 correspond au format standard combined. Beaucoup de 504 pour peu de 499 indiquent quelques pages lourdes isolées, l'image inverse trahit une application poussive de bout en bout.

Avant toute modification : le chemin du retour

Le diagnostic des deux sections suivantes est purement en lecture. Le risque commence seulement là où vous touchez aux configurations, et cela peut arrêter l'exploitation de trois façons : une configuration nginx erronée empêche le démarrage du serveur web, un fichier de pool erroné celui de PHP-FPM, et des limites relevées trop généreusement peuvent épuiser la RAM. Le dernier cas est le plus désagréable, car le noyau met alors fin à des processus, et ce n'est pas forcément le fautif qui est touché. Si le service SSH y passe, le serveur n'est plus pilotable par le réseau.

Commencez donc par créer des copies, sous /root et jamais dans le répertoire web. L'horodatage dans le nom compte, car on intervient rarement une seule fois et cp -a écrase une copie existante sans le moindre avertissement :

mkdir -p /root/backups
cp -a /etc/nginx/nginx.conf /root/backups/nginx.conf.$(date +%F-%H%M)
cp -a /etc/nginx/sites-available/example.com /root/backups/example.com.$(date +%F-%H%M)
cp -a /etc/php/8.4/fpm/php.ini /root/backups/php.ini.$(date +%F-%H%M)
cp -a /etc/php/8.4/fpm/pool.d/www.conf /root/backups/www.conf.$(date +%F-%H%M)

Le chemin du retour tient en trois lignes, et l'ordre n'a rien d'accidentel :

cp -a /root/backups/example.com.2026-09-03-1030 /etc/nginx/sites-available/example.com
nginx -t
systemctl reload nginx

Utilisez reload plutôt que restart tant que c'est possible. reload ne reprend la nouvelle configuration que si elle est exempte d'erreur. Un restart arrête d'abord le processus en cours et vous laisse sans serveur web en cas d'erreur.

Si le serveur ne répond plus du tout, ouvrez la console VNC dans l'espace client, sur les serveurs root KVM et les serveurs dédiés de KernelHost. Elle est rattachée à la couche de virtualisation, ou au raccordement lui-même, et non à la pile réseau du système invité : elle fonctionne donc encore quand plus aucun service n'est joignable. Connectez-vous-y une fois à l'avance et assurez-vous de connaître le mot de passe root. Commande de contrôle après chaque intervention :

systemctl is-active nginx php8.4-fpm
free -m

La ligne du journal d'erreurs qui tranche le cas

Un 504 laisse toujours une trace :

2026/09/03 10:12:33 [error] 812#812: *5 upstream timed out (110: Connection timed out)
while reading response header from upstream, client: 203.0.113.7, server: example.com,
request: "GET /report.php HTTP/1.1", upstream: "fastcgi://unix:/run/php/php8.4-fpm.sock:"

Ce qui est décisif n'est pas le numéro d'erreur 110, identique pour tous les 504, mais la phase indiquée juste après :

  • while connecting to upstream : la connexion n'a pas pu être établie. Avec un backend distant, c'est presque toujours un filtre de paquets qui jette les paquets au lieu de les rejeter, car un rejet reviendrait immédiatement et donnerait un 502.
  • while sending request to upstream : nginx n'a pas réussi à écouler le corps de la requête, typique des envois de fichiers volumineux.
  • while reading response header from upstream : le cas normal. Le backend a tout reçu et calcule sans envoyer la moindre ligne d'en-tête.
  • while reading upstream : les en-têtes sont arrivés, puis le corps s'est bloqué. Vous voyez cela sur les réponses en streaming et sur les exports.
grep -n "upstream timed out" /var/log/nginx/error.log | tail -20

Beaucoup d'hôtes virtuels écrivent dans un journal d'erreurs qui leur est propre. Où se trouve ce fichier et pourquoi le -R majuscule est nécessaire pour chercher dans sites-enabled, c'est expliqué dans l'article consacré au 502. Si la recherche reste vide alors que le navigateur affiche un 504, l'erreur ne vient pas de ce nginx.

Où le temps se consomme : mesurer au lieu de deviner

nginx sait consigner, pour chaque requête, le temps qu'a pris le backend. C'est l'étape la plus importante du diagnostic, parce qu'elle répond sans supposition à la question « application ou liaison ». Dans le bloc http de /etc/nginx/nginx.conf :

log_format kh_timing '$time_iso8601 $status rt=$request_time '
                     'uct=$upstream_connect_time uht=$upstream_header_time '
                     'urt=$upstream_response_time "$request"';

Dans le bloc server concerné, ajoutez une seconde ligne de journalisation. Le journal d'accès existant reste intact, nginx écrit dans les deux :

access_log /var/log/nginx/timing.log kh_timing;
nginx -t
systemctl reload nginx
tail -n 5 /var/log/nginx/timing.log

Après quelques minutes d'exploitation, faites remonter les requêtes les plus lentes :

awk '{ t=$3; sub(/^rt=/, "", t); print t, $0 }' /var/log/nginx/timing.log | sort -rn | head -20
ObservationInterprétationÉtape suivante
uct élevé avec un backend localL'établissement de la connexion coinceFile d'attente pleine sur le socket, résolution de noms lente
uht et urt presque identiques, tous deux élevésLe backend calcule avant d'envoyer la première ligne d'en-têteApplication, base de données, interface tierce
uht faible, urt élevéL'en-tête est arrivé vite, le corps arrive au goutte-à-goutteStreaming, exports, boucles sur de nombreux enregistrements
urt faible, rt élevéLe backend a été rapide, le temps s'est perdu ensuiteLiaison du visiteur, réponse très volumineuse
Un tiret au lieu d'un nombreAucun backend n'est intervenuFichier statique ou abandon avant la transmission

Deux subtilités font gagner beaucoup de temps. Plusieurs valeurs séparées par des virgules dans un même champ signifient que la requête est partie vers plus d'une cible, il y a donc eu une nouvelle tentative. Et l'indice le plus parlant de tous : si la durée mesurée correspond à la seconde près à la valeur configurée, par exemple 60,001 secondes pour un délai de 60, c'est le délai qui a frappé et non le backend qui a renoncé de lui-même. Des valeurs bancales comme 43,7 secondes montrent qu'autre chose a freiné.

Quelle limite de temps s'applique réellement

nginx propose une bonne douzaine de directives pour les dépassements de délai, et l'heure la plus souvent perdue vient de ce que quelqu'un a touché la mauvaise. Celle qui s'applique dépend du module qui traite le bloc location.

DirectiveValeur par défautS'applique dans les blocs avecEffet à l'expiration
proxy_connect_timeout60sproxy_pass504, « while connecting to upstream »
proxy_send_timeout60sproxy_pass504, « while sending request to upstream »
proxy_read_timeout60sproxy_pass504, le délai décisif avec les backends en proxy
fastcgi_connect_timeout60sfastcgi_pass504, comme ci-dessus, pour PHP-FPM
fastcgi_send_timeout60sfastcgi_pass504, comme ci-dessus
fastcgi_read_timeout60sfastcgi_pass504, le délai décisif avec PHP
send_timeout60spartoutpas de 504, la connexion vers le visiteur est fermée
client_body_timeout60spartout408, pas 504

Le délai s'applique entre deux lectures, pas à la réponse entière. Un téléchargement qui dure dix minutes en livrant des données sans interruption passe sans problème. Un backend qui se tait pendant 61 secondes est éjecté. Sur les exports qui marquent des pauses, il est donc souvent plus utile de faire écrire régulièrement quelque chose à l'application que d'augmenter le délai.

send_timeout ne fait rien contre un 504. Ce délai concerne la transmission vers le visiteur. À son expiration, il n'y a pas de page d'erreur mais un téléchargement interrompu. Il ne devient pertinent que si vous désactivez la mise en tampon avec proxy_buffering off;, car un visiteur lent freine alors jusque dans le backend.

nginx accepte aussi des directives qui restent sans effet dans le bloc concerné. Un proxy_read_timeout 300s; dans un bloc PHP avec fastcgi_pass est syntaxiquement irréprochable, nginx -t répond syntax is ok, et la page continue de s'interrompre au bout de 60 secondes. L'inverse est tout aussi vrai. C'est la cause la plus fréquente d'un délai relevé qui reste sans effet. Ce qui s'applique réellement, la configuration assemblée le montre :

nginx -T | grep -E "read_timeout|send_timeout|fastcgi_pass|proxy_pass"

Si un chemin isolé doit réellement s'exécuter plus longtemps, réglez le délai exactement là et nulle part ailleurs. Le signe égal en fait une correspondance exacte, qui l'emporte sur le bloc général location ~ \.php$ chargé des autres fichiers PHP :

location = /admin/export.php {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
    fastcgi_read_timeout 300s;
}
nginx -t
systemctl reload nginx
curl -s -o /dev/null -w "%{http_code} %{time_total}\n" -H "Host: example.com" http://127.0.0.1/admin/export.php

PHP : pourquoi max_execution_time s'applique rarement ici

L'hypothèse la plus naturelle veut que PHP mette fin de lui-même à un script bloqué. Dans ce cas précis, c'est le plus souvent faux. D'abord le piège de mesure : php -i interroge la variante en ligne de commande. Celle-ci utilise sa propre configuration sous /etc/php/8.4/cli/ et tourne de toute façon sans limite d'exécution, elle ne dit donc rien de FPM. Les deux endroits qui font foi sont ceux-ci :

grep -n "^max_execution_time" /etc/php/8.4/fpm/php.ini
grep -rn "max_execution_time" /etc/php/8.4/fpm/pool.d/

Dans le fichier de pool, la valeur peut être écrasée par php_value[max_execution_time] ou par php_admin_value[max_execution_time]. Une valeur posée avec php_admin_value ne peut plus être modifiée depuis l'application avec ini_set(). Si votre framework relève lui-même la durée d'exécution et que cela reste soudain sans effet, l'explication est là.

Et maintenant le vrai point : sous Linux, le temps d'attente passé dans les appels système ne compte pas. L'horloge ne tourne que tant que le script calcule lui-même. S'il attend une requête de base de données, une interface tierce ou le système de fichiers, elle s'arrête. Un script peut ainsi rester collé dix minutes à une requête bloquée sans que la limite d'exécution intervienne jamais. C'est alors le délai de nginx qui y met fin, et le résultat est le 504.

Il en découle une règle inconfortable : max_execution_time protège des boucles infinies dans votre propre code, pas de l'attente. La seule limite dure du côté de PHP est request_terminate_timeout dans le fichier de pool, qui balaie le processus de travail quelle que soit la raison de son blocage. Elle produit toutefois un 502 et non un 504. Ordonnez les délais en croissant, de l'intérieur vers l'extérieur, pour que la couche qui peut encore produire un message compréhensible intervienne en premier. Cela fonctionne pour les scripts qui calculent, mais pas pour ceux qui attendent, exactement pour la raison qui vient d'être donnée. Là, il ne reste qu'à borner l'attente elle-même.

Pourquoi relever le délai est le plus souvent la mauvaise réponse

Faisons rapidement le calcul. Un pool avec pm.max_children = 10 dispose de dix processus de travail. Une page met 90 secondes. Dix appels simultanés occupent donc chacun d'eux pendant une minute et demie. Pendant ce temps, plus personne ne reçoit de page PHP, pas même la page d'accueil. Une sous-page lente s'est transformée en panne. Trois mécanismes amplifient le phénomène :

  • Le visiteur recharge. Cela crée une requête supplémentaire sans libérer l'ancienne. PHP ne remarque le départ d'un visiteur qu'à la sortie suivante du script, et un script qui calcule ne produit rien pendant longtemps.
  • nginx retente de lui-même. Dans un bloc upstream comportant plusieurs cibles, proxy_next_upstream vaut error timeout par défaut. Une requête expirée part vers le serveur suivant et la requête coûteuse s'exécute une seconde fois. Les requêtes en écriture sont exclues, celles en lecture non. On désactive cela avec proxy_next_upstream error;.
  • La supervision retente elle aussi. Un intervalle de contrôle de 60 secondes sur une page qui met 90 secondes crée une charge permanente qui n'est jamais résorbée.

S'y ajoute que personne n'attend cinq minutes devant une page web. Un délai de 300 secondes transforme un problème d'une minute en un problème de cinq minutes, et le visiteur est parti depuis longtemps pendant que le processus de travail continue de calculer.

Le niveau de saturation réel, c'est la page d'état de PHP-FPM qui le montre. Posez pm.status_path = /fpm-status dans le fichier de pool et créez dans le bloc server un accès joignable uniquement en local :

location = /fpm-status {
    allow 127.0.0.1;
    deny all;
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
}
systemctl reload php8.4-fpm
nginx -t
systemctl reload nginx
curl -s -H "Host: example.com" http://127.0.0.1/fpm-status

Surveillez active processes, listen queue et max active processes. Si listen queue reste durablement au-dessus de zéro, le nombre de processus de travail ne suffit pas pour le temps d'exécution actuel des pages. C'est le moment de réduire ce temps d'exécution, pas d'augmenter le délai.

Les trois dévoreurs de temps habituels

Base de données

Dans la majorité des cas, le temps se cache ici. Regardez d'abord ce qui tourne en ce moment. Sur les quatre systèmes, l'accès root passe par défaut par le socket Unix, la commande se passe donc de mot de passe :

mysql -e "SHOW FULL PROCESSLIST;"

Les colonnes Time et State sont les plus parlantes. Des valeurs comme Sending data ou Waiting for table metadata lock avec des durées à deux chiffres en secondes, c'est votre cas. Pour chercher de façon systématique, servez-vous du journal des requêtes lentes, activable sans interrompre le service :

mysql -e "SET GLOBAL slow_query_log = 1; SET GLOBAL long_query_time = 1;"
mysql -e "SHOW VARIABLES LIKE 'slow_query_log_file';"

Le nom du fichier se lit dans la seconde sortie, car il varie : Debian mise habituellement sur MariaDB et écrit dans /var/log/mysql/mariadb-slow.log, tandis que sous Ubuntu le fichier porte un autre nom selon le serveur installé. Analysez au bout de quelques minutes puis désactivez de nouveau, car ce journal coûte des écritures :

mysqldumpslow -s t /var/log/mysql/mariadb-slow.log | head -30
mysql -e "SET GLOBAL slow_query_log = 0;"

L'option -s t trie par temps total. Vérifiez la requête la plus coûteuse avec EXPLAIN : il manque le plus souvent un index sur la colonne même qui sert au filtrage ou au tri. SET GLOBAL prend effet immédiatement, mais ne survit pas à un redémarrage de la base de données. Une limite haute par requête existe aussi du côté de la base : MariaDB connaît max_statement_time en secondes, MySQL max_execution_time en millisecondes, ce dernier uniquement pour les requêtes en lecture. Votre application reçoit ainsi une erreur propre au lieu d'un processus de travail occupé.

Interfaces tierces

Si votre site interroge un prestataire de paiement ou un serveur de licences à chaque appel, leur panne devient votre 504. Mesurez cet appel séparément, depuis le serveur :

curl -o /dev/null -s -w "dns=%{time_namelookup} connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n" https://api.example.com/status

Si dns sort déjà du lot, c'est la résolution de noms qui est en cause et non le correspondant ; contre-épreuve avec time getent hosts api.example.com. Dans le code, chaque appel externe a besoin de son propre délai, court : avec cURL, ce sont CURLOPT_CONNECTTIMEOUT et CURLOPT_TIMEOUT. Qui applique plutôt file_get_contents() à une adresse atterrit sur default_socket_timeout du php.ini, qui vaut 60 secondes par défaut :

grep -n "default_socket_timeout" /etc/php/8.4/fpm/php.ini

La règle empirique : la somme de tous les délais externes d'une requête doit rester sous fastcgi_read_timeout. Sinon nginx coupe avant que votre code ait pu afficher une page d'erreur compréhensible.

Système de fichiers et RAM

Un disque plein rend les écritures lentes ou impossibles, et cela touche en même temps les fichiers de session, les caches et les journaux. Vérifiez les deux, l'espace et les inodes :

df -h
df -i

La seconde commande est celle qu'on oublie souvent. Une partition peut être occupée à 40 % et refuser malgré tout le moindre fichier supplémentaire dès que les inodes sont épuisés. Le deuxième candidat est le manque de mémoire : si le système déporte sur le disque, chaque requête devient poussive sans qu'une requête précise en soit responsable.

vmstat 1 5

Si les colonnes si et so restent durablement au-dessus de zéro, le noyau écrit et relit sans arrêt sur le disque, et vous avez un problème de mémoire et non un problème de temps. La bonne façon de traiter cela est décrite dans configurer le swap et éviter les plantages par manque de mémoire. Le troisième candidat, ce sont les lecteurs réseau : un point de montage NFS bloqué fige tout processus qui y touche, et votre commande de diagnostic en fait partie. Placez donc un délai devant :

timeout 5 df -h

Les tâches longues n'ont pas leur place dans la requête

Certaines tâches prennent du temps, c'est ainsi : un rapport annuel, un import de 200 000 lignes, une conversion d'images. L'erreur n'est pas qu'elles durent longtemps, mais qu'un serveur web les attende. La forme propre comporte trois parties :

  1. La requête crée une tâche, dans une table ou dans une file d'attente, et répond immédiatement. Le code de statut approprié est 202, accompagné d'une adresse où l'état peut être consulté.
  2. Un processus de travail situé en dehors de nginx récupère les tâches et les exécute. Aucun délai ne lui souffle dans le cou, puisque personne ne l'attend.
  3. L'interface interroge l'état. Cette requête est toujours rapide, quelle que soit la durée de la tâche.

Faites tourner ce processus de travail comme un service à part entière, pour qu'il revienne de lui-même après un plantage et après un redémarrage. À quoi ressemble une telle unit, c'est ce que montre créer son propre service systemd. Pour des tâches à intervalles fixes, un cron job suffit ; il vous y faut un verrou pour que deux exécutions ne se doublent pas. Avec -n, la seconde exécution s'arrête immédiatement au lieu d'attendre :

flock -n /run/lock/kh-worker.lock /usr/bin/php /var/www/html/worker.php

Pour les frameworks courants, la file d'attente existe déjà, il ne reste qu'à la faire tourner : chez Laravel php artisan queue:work, chez Symfony php bin/console messenger:consume avec le nom de votre transport. Les deux ont leur place dans une unit systemd, pas dans une fenêtre de terminal.

Un cas particulier mérite une mention explicite : par défaut, WordPress lance les tâches planifiées à l'intérieur des requêtes des visiteurs. Un visiteur paie donc de son temps d'attente le fait qu'une vérification de mises à jour tourne en arrière-plan. La ligne define('DISABLE_WP_CRON', true); dans wp-config.php, au-dessus de l'appel à wp-settings.php, désactive ce comportement. Ensuite, appelez vous-même les tâches dues à intervalles réguliers :

wp cron event run --due-now --path=/var/www/html

Ce qui doit impérativement rester synchrone reçoit son propre bloc location avec son propre délai, et en plus une limite pour que ce seul chemin n'occupe pas tous les processus de travail : limit_conn_zone $binary_remote_addr zone=export:10m; dans le bloc http et limit_conn export 1; dans le bloc concerné. Les tentatives supplémentaires reçoivent alors un 503 au lieu d'un serveur saturé.

Erreurs fréquentes et solutions

Message mot pour motSignification et remède
upstream timed out (110: Connection timed out) while reading response header from upstreamLe cas normal. Le backend calcule trop longtemps. Analysez le journal des temps et identifiez le dévoreur de temps avant de toucher au délai.
upstream timed out (110: Connection timed out) while connecting to upstreamL'établissement de la connexion est allé jusqu'au bout du délai. Avec un backend distant, c'est presque toujours un filtre de paquets qui jette au lieu de rejeter, avec un PHP-FPM local, une file d'attente pleine sur le socket.
upstream timed out (110: Connection timed out) while reading upstreamLes en-têtes sont arrivés, puis le corps s'est bloqué plus longtemps que le délai. Typique des exports qui calculent longuement entre deux envois.
nginx: [emerg] "fastcgi_read_timeout" directive is not allowed here in /etc/nginx/nginx.conf:12La directive se trouve en dehors de http, server ou location, le plus souvent par mégarde tout en haut du fichier.
nginx: [emerg] unknown directive "proxy_read_timout" in /etc/nginx/sites-enabled/example.com:31Faute de frappe. nginx vérifie des noms, pas des intentions. Le numéro de ligne figure dans le message.
PHP Fatal error: Maximum execution time of 30 seconds exceeded in /var/www/html/export.php on line 42Ici, la limite d'exécution de PHP a exceptionnellement fonctionné : le script calculait donc, il n'attendait pas. Cela donne un 500 ou une page vide, pas un 504.
SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transactionUne autre transaction retient la ligne, le délai vaut 50 secondes par défaut. La cause est presque toujours une transaction restée ouverte trop longtemps.
cURL error 28: Operation timed out after 60000 millisecondsUne interface tierce ne répond pas. Posez dans le code un délai à vous, plus court, pour que votre application garde la main.
504 dans le navigateur, mais la recherche de upstream timed out reste videLe 504 ne vient pas de ce nginx mais d'un service placé en amont, comme un load balancer ou un second proxy. Certains signalent cela avec un code de statut qui leur est propre.
Interruption toujours au bout de 60 secondes exactement, alors que le délai est réglé sur 300La directive modifiée appartient au mauvais module, ou un autre bloc l'emporte. nginx -T montre ce qui s'applique réellement.

Comment reconnaître que le problème est réglé

Une commande sans message d'erreur ne prouve rien, un appel réussi isolé non plus. Quatre preuves qui, ensemble, tiennent :

  1. Code de statut et durée directement sur le serveur, pour qu'aucun cache n'embellisse le résultat. On attend un 200 et une durée nettement inférieure au délai. Un 200 au bout de 58 secondes pour un délai de 60 n'est pas un succès, c'est la prochaine panne dès que la charge monte un peu. Ce qui compte, c'est le pire de vingt appels :
    for i in $(seq 1 20); do curl -s -o /dev/null -w "%{http_code} %{time_total}\n" -H "Host: example.com" http://127.0.0.1/report.php; done | sort -k2 -n | tail -3
  2. Le journal d'erreurs reste muet. Videz-le avant le test avec truncate -s 0 /var/log/nginx/error.log, déclenchez les appels, puis regardez de nouveau dedans. Un fichier vide, voilà la vraie preuve.
  3. La file d'attente de PHP-FPM est à zéro. Tant que quelque chose attend sous listen queue sur la page d'état, la cause n'est que déplacée.
  4. Un redémarrage ne change rien. C'est le point le plus souvent sauté. Les valeurs issues de SET GLOBAL, les processus lancés à la main et les répertoires créés soi-même sous /run n'y survivent pas. Vérifiez systemctl is-enabled nginx php8.4-fpm et redémarrez le serveur une fois de façon contrôlée, tant que vous êtes encore devant.

Sur les serveurs root KVM et les serveurs dédiés de KernelHost, ce redémarrage et l'accès console se font depuis l'espace client, même quand le service web ne livre plus rien. Les serveurs se trouvent dans le datacenter maincubes à Francfort-sur-le-Main (certifié TÜV TIER3+), avec un filtrage placé en amont dans le réseau.

Liste de contrôle rapide en cas d'urgence

  1. Compter les codes de statut dans le journal d'accès. Vus côte à côte, 504, 499 et 502 en disent plus que chacun pris isolément.
  2. grep "upstream timed out" /var/log/nginx/error.log, noter la phase indiquée dans le message.
  3. Activer le journal des temps, comparer uct, uht et urt. Si la durée colle à la seconde près au délai configuré, c'est le délai qui a frappé.
  4. Identifier le dévoreur de temps : base de données, interface tierce, système de fichiers ou mémoire.
  5. Vérifier avec nginx -T quel délai s'applique dans ce bloc avant d'en modifier un.
  6. Décider seulement ensuite : corriger, déplacer vers une file d'attente ou, en dernier recours, relever le délai pour ce seul chemin.
  7. Après le correctif : vider le journal, mesurer vingt appels, redémarrer une fois le serveur de façon contrôlée.

Questions fréquentes

Quelle est la différence entre 502 Bad Gateway et 504 Gateway Time-out ?
Avec un 502, la connexion vers le backend ne s'établit pas ou elle est coupée : le backend répond donc mal ou pas du tout. Avec un 504, la connexion tenait, seule la réponse n'est pas arrivée dans le délai : le backend est resté joignable pendant tout ce temps et il a répondu trop lentement. Un 500 signifie en revanche que le backend a répondu et que sa réponse était une erreur. La direction des recherches en découle : avec un 502, vous vérifiez le service, le socket et les droits, avec un 504, le temps d'exécution dans le backend.
J'ai réglé fastcgi_read_timeout sur 300 secondes, la page s'interrompt malgré tout au bout de 60 secondes. D'où cela vient-il ?
Presque toujours du fait que la directive modifiée appartient au mauvais module, ou qu'un autre bloc l'emporte. nginx accepte aussi des directives qui restent sans effet dans le bloc concerné : un proxy_read_timeout 300s; dans un bloc PHP avec fastcgi_pass est syntaxiquement irréprochable, nginx -t répond « syntax is ok », et la page continue de s'interrompre au bout de 60 secondes. L'inverse est tout aussi vrai. Ce qui s'applique réellement, la configuration assemblée le montre, avec nginx -T et une recherche sur read_timeout, send_timeout, fastcgi_pass et proxy_pass. Si un chemin isolé doit s'exécuter plus longtemps, réglez le délai dans un bloc location à lui, avec le signe égal, car une correspondance exacte l'emporte sur le bloc PHP général.
Un délai plus long résout-il le problème ?
Le plus souvent, il ne fait que le repousser. Un pool avec pm.max_children = 10 dispose de dix processus de travail. Si une page met 90 secondes, dix appels simultanés occupent chacun d'eux pendant une minute et demie, et pendant ce temps plus personne ne reçoit de page PHP, pas même la page d'accueil. Une sous-page lente s'est ainsi transformée en panne. Ajoutez à cela que personne n'attend cinq minutes devant une page web. Si le 504 disparaît après que vous avez relevé le délai et que des 499 apparaissent à la place dans le journal d'accès, c'est le visiteur lui-même qui a abandonné et rien n'est résolu.
Quelle ligne du journal d'erreurs correspond à un 504 ?
Un 504 laisse toujours une ligne de la forme « upstream timed out (110: Connection timed out) while reading response header from upstream », que l'on trouve avec grep -n "upstream timed out" /var/log/nginx/error.log. Ce qui est décisif n'est pas le numéro d'erreur 110, identique pour tous les 504, mais la phase indiquée juste après : « while connecting to upstream » signale une connexion qui n'a pas pu être établie, « while sending request to upstream » que nginx n'a pas réussi à écouler le corps de la requête, « while reading response header from upstream » le cas normal, où le backend calcule sans envoyer la moindre ligne d'en-tête, et « while reading upstream » que le corps s'est bloqué après les en-têtes.
Pourquoi max_execution_time ne met-il pas fin à mon script PHP bloqué ?
Parce que sous Linux, le temps d'attente passé dans les appels système ne compte pas. L'horloge ne tourne que tant que le script calcule lui-même. S'il attend une requête de base de données, une interface tierce ou le système de fichiers, elle s'arrête, et le script peut rester collé dix minutes à une requête bloquée sans que la limite d'exécution intervienne jamais. C'est alors le délai de nginx qui y met fin, et le résultat est le 504. Attention aussi au piège de mesure : php -i interroge la variante en ligne de commande, qui utilise sa propre configuration et tourne de toute façon sans limite d'exécution. Ce qui fait foi, c'est le php.ini de FPM et le fichier de pool. La seule limite dure du côté de PHP est request_terminate_timeout, mais elle produit un 502 et non un 504.
Comment savoir si c'est le délai qui a frappé ou le backend qui a renoncé de lui-même ?
À la durée mesurée. Consignez dans un fichier supplémentaire, au moyen d'un log_format à vous, les valeurs $request_time, $upstream_connect_time, $upstream_header_time et $upstream_response_time. Si la durée correspond à la seconde près à la valeur configurée, par exemple 60,001 secondes pour un délai de 60, c'est le délai qui a frappé et non le backend qui a renoncé de lui-même. Des valeurs bancales comme 43,7 secondes montrent qu'autre chose a freiné. Plusieurs valeurs séparées par des virgules dans un même champ signalent une nouvelle tentative vers une seconde cible, un tiret au lieu d'un nombre signifie qu'aucun backend n'est intervenu.
Le navigateur affiche un 504, mais la recherche de « upstream timed out » reste vide. D'où vient l'erreur ?
Le 504 ne vient alors pas de ce nginx, mais d'un service placé en amont, comme un load balancer ou un second proxy, et certains signalent cela avec un code de statut qui leur est propre. Vérifiez cependant un point auparavant : beaucoup d'hôtes virtuels écrivent dans un journal d'erreurs qui leur est propre, vous cherchez donc peut-être dans le mauvais fichier.
Que faire des tâches qui durent par nature plus longtemps que n'importe quel délai raisonnable ?
Sortez-les de la requête. La requête crée une tâche et répond immédiatement, avec le code de statut 202 qui convient et une adresse où l'état peut être consulté. Un processus de travail situé en dehors de nginx exécute la tâche, sans délai qui lui souffle dans le cou, et l'interface ne fait plus qu'interroger l'état. Faites tourner ce processus de travail comme un service à part entière, pour qu'il revienne de lui-même après un plantage et après un redémarrage, et protégez les exécutions récurrentes avec flock -n pour que deux exécutions ne se doublent pas. Ce qui doit impérativement rester synchrone reçoit son propre bloc location avec son propre délai, et en plus une limite au moyen de limit_conn pour que ce seul chemin n'occupe pas tous les processus de travail.

nginx PHP-FPM 504 Gateway Time-out Timeout Debian Ubuntu Dépannage Administration Linux