nginx 504 Gateway Time-out: znajdź przyczynę zamiast podnosić limit

Opublikowano 16 min czytania

Przy błędzie 504 backend był osiągalny, tylko odpowiedział za wolno. Jak z pomocą logu czasów ustalić, gdzie zużywa się czas, który z wielu limitów naprawdę działa i dlaczego wyższy limit najczęściej tylko przesuwa awarię.

Błąd 504 Gateway Time-out to najbardziej cierpliwa ze wszystkich stron błędu. nginx przyjął żądanie, przekazał je do backendu, a potem czekał, aż upłynął ustawiony wewnętrznie limit czasu. Backend był przez cały czas osiągalny, po prostu nie odpowiedział na czas. Właśnie tu leży różnica wobec sąsiedniego kodu: przy 502 Bad Gateway backend odpowiada błędnie albo wcale, przy 504 odpowiada za wolno. Ten poradnik pokazuje, jak zmierzyć, gdzie naprawdę zużywa się czas, który z wielu limitów w ogóle zadziałał i dlaczego podnoszenie tego limitu jest prawie zawsze najgorszą z dostępnych odpowiedzi.

Wszystkie informacje dotyczą systemów Debian 13 (trixie), Debian 12 (bookworm), Ubuntu 24.04 LTS oraz Ubuntu 22.04 LTS. Polecenia napisano z myślą o pracy jako root, jako zwykły użytkownik dopisz z przodu sudo. W przykładach występuje PHP 8.4, zastąp numer wersji tym z twojego systemu:

SystemPHPUsługaKonfiguracja
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/

Dyrektywy nginx nazywają się tak samo we wszystkich czterech systemach. Różnice leżą po stronie PHP oraz bazy danych i są zaznaczone w odpowiednich miejscach.

To, kto się poddał, wyznacza kierunek poszukiwań

Zanim otworzysz jakikolwiek plik, odpowiedz na jedno pytanie: która warstwa przerwała? Kod statusu już to zdradza.

KodCo się stałoGdzie szukać
500 Internal Server ErrorBackend odpowiedział, a odpowiedzią był błądLog aplikacji
502 Bad GatewayPołączenie nie doszło do skutku albo się zerwałoUsługa, socket, uprawnienia, awarie
504 Gateway Time-outPołączenie stało, odpowiedź nie przyszła w wyznaczonym czasieCzas działania w backendzie
408 Request TimeoutOdwiedzający nie dosłał na czas własnego żądania do końcaWysyłanie plików, wolne łącza
499 (tylko w logu)Odwiedzający przerwał, zanim nginx skończyłZa wolno, ale poniżej limitu

Wiersz z kodem 499 jest najbardziej niedoceniany. To nie błąd, tylko system wczesnego ostrzegania: odwiedzający zamknął kartę, bo strona ładowała się zbyt długo. Jeśli po podniesieniu limitu błąd 504 znika, a w jego miejsce pojawiają się kody 499, nic nie zostało rozwiązane.

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

Pole numer 9 obowiązuje dla standardowego formatu combined. Dużo kodów 504 przy niewielu 499 wskazuje na pojedyncze ciężkie podstrony, obraz odwrotny na aplikację ociężałą w całości.

Zanim cokolwiek zmienisz: droga powrotna

Diagnostyka z dwóch następnych sekcji tylko czyta. Ryzykownie robi się dopiero tam, gdzie ruszasz konfiguracje, a to potrafi zatrzymać pracę na trzy sposoby: błędna konfiguracja nginx uniemożliwia start serwera WWW, błędny plik puli blokuje start PHP-FPM, a hojnie podniesione limity potrafią wyczerpać pamięć RAM. Ostatni przypadek jest najbardziej dokuczliwy, bo wtedy kernel zabija procesy, i niekoniecznie trafia na sprawcę. Jeśli oberwie usługa SSH, serwera nie da się już obsłużyć przez sieć.

Dlatego najpierw zrób kopie, poniżej katalogu /root i nigdy w katalogu WWW. Znacznik czasu w nazwie jest ważny, bo rzadko kiedy ingeruje się tylko raz, a cp -a nadpisuje istniejącą kopię bez słowa ostrzeżenia:

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)

Droga powrotna to trzy wiersze, a ich kolejność nie jest przypadkowa:

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

Używaj reload zamiast restart tak długo, jak się da. reload przejmuje nową konfigurację tylko wtedy, gdy jest bezbłędna. restart najpierw kończy działający proces i przy błędzie zostawia cię bez serwera WWW.

Jeśli serwer w ogóle przestanie odpowiadać, przy serwerach root KVM i serwerach dedykowanych od KernelHost otwórz konsolę VNC w panelu klienta. Wisi ona na warstwie wirtualizacji, względnie na samym łączu, a nie na stosie sieciowym systemu gościa, działa więc również wtedy, gdy żadna usługa nie jest już osiągalna. Zaloguj się tam raz zawczasu i upewnij się, że znasz hasło roota. Polecenie kontrolne po każdej ingerencji:

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

Wiersz w logu błędów, który rozstrzyga sprawę

Błąd 504 zawsze zostawia ślad:

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:"

Decydujący nie jest numer błędu 110, który przy każdym 504 brzmi tak samo, tylko faza wymieniona za nim:

  • while connecting to upstream: nawiązanie połączenia nie doszło do skutku. Przy zdalnym backendzie prawie zawsze odpowiada za to filtr pakietów, który pakiety porzuca, zamiast je odrzucić, bo odrzucenie wróciłoby natychmiast i dałoby błąd 502.
  • while sending request to upstream: nginx nie zdołał przekazać dalej treści żądania, typowe przy dużych plikach wysyłanych na serwer.
  • while reading response header from upstream: przypadek typowy. Backend dostał wszystko i liczy, nie wysyłając nawet pierwszego wiersza nagłówka.
  • while reading upstream: nagłówki przyszły, potem zaciął się korpus odpowiedzi. Zobaczysz to przy odpowiedziach strumieniowych i przy eksportach.
grep -n "upstream timed out" /var/log/nginx/error.log | tail -20

Wiele wirtualnych hostów pisze do własnego logu błędów. Gdzie leży ten plik i dlaczego przy przeszukiwaniu katalogu sites-enabled potrzebne jest wielkie -R, opisuje artykuł o błędzie 502. Jeśli wyszukiwanie nic nie znajduje, choć przeglądarka pokazuje 504, błąd nie pochodzi z tego nginx.

Gdzie zużywa się czas: mierz, nie zgaduj

nginx potrafi zapisywać dla każdego żądania, ile czasu potrzebował backend. To najważniejszy krok diagnostyki, bo bez zgadywania odpowiada na pytanie „aplikacja czy łącze”. W bloku http pliku /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"';

W odpowiednim bloku server dopisz drugi wiersz logowania. Istniejący log dostępu pozostaje nietknięty, nginx pisze do obu:

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

Po kilku minutach pracy wyciągnij na wierzch najwolniejsze żądania:

awk '{ t=$3; sub(/^rt=/, "", t); print t, $0 }' /var/log/nginx/timing.log | sort -rn | head -20
ObserwacjaInterpretacjaNastępny krok
wysokie uct przy lokalnym backendzieNawiązanie połączenia się zacinaPełna kolejka na sockecie, wolne rozwiązywanie nazw
uht i urt niemal równe, oba wysokieBackend liczy, zanim wyśle pierwszy wiersz nagłówkaAplikacja, baza danych, zewnętrzne API
uht małe, urt wysokieNagłówek przyszedł szybko, korpus sączy się kroplamiStrumieniowanie, eksporty, pętle po wielu rekordach
urt małe, rt wysokieBackend był szybki, czas przepadł późniejŁącze odwiedzającego, bardzo duża odpowiedź
Myślnik zamiast liczbyŻaden backend nie brał w tym udziałuPlik statyczny albo przerwanie przed przekazaniem dalej

Dwa szczegóły oszczędzają mnóstwo czasu. Kilka wartości rozdzielonych przecinkiem w jednym polu oznacza, że żądanie poszło do więcej niż jednego celu, doszło więc do ponowienia. I najostrzejsza wskazówka ze wszystkich: jeśli zmierzony czas co do sekundy odpowiada skonfigurowanej wartości, na przykład 60,001 sekundy przy limicie 60, to zadziałał limit, a backend nie poddał się sam z siebie. Niepełne wartości w rodzaju 43,7 sekundy pokazują, że hamowało coś innego.

Który limit czasu w ogóle zadziała

nginx ma na przekroczenia czasu dobre kilkanaście dyrektyw, a najczęściej zmarnowana godzina bierze się stąd, że ktoś rusza niewłaściwą. To, która zadziała, zależy od tego, który moduł obsługuje dany blok location.

DyrektywaDomyślnieGdzie działaSkutek po upływie
proxy_connect_timeout60sproxy_pass504, „while connecting to upstream”
proxy_send_timeout60sproxy_pass504, „while sending request to upstream”
proxy_read_timeout60sproxy_pass504, decydujący limit przy backendach za proxy
fastcgi_connect_timeout60sfastcgi_pass504, jak wyżej, dla PHP-FPM
fastcgi_send_timeout60sfastcgi_pass504, jak wyżej
fastcgi_read_timeout60sfastcgi_pass504, decydujący limit przy PHP
send_timeout60swszędziebrak 504, zamykane jest połączenie z odwiedzającym
client_body_timeout60swszędzie408, nie 504

Limit obowiązuje między dwoma odczytami, a nie dla całej odpowiedzi. Pobieranie, które trwa dziesięć minut i przez cały ten czas nieprzerwanie dostarcza dane, przejdzie bez przeszkód. Backend, który milczy 61 sekund, wylatuje. Przy zacinających się eksportach więcej daje więc regularne wypisywanie czegokolwiek przez aplikację niż podnoszenie limitu.

send_timeout nic nie zmienia przy błędzie 504. Ten limit dotyczy transmisji do odwiedzającego. Gdy upłynie, nie ma strony błędu, tylko przerwane pobieranie. Znaczenia nabiera dopiero wtedy, gdy wyłączysz buforowanie przez proxy_buffering off;, bo wtedy wolny odwiedzający hamuje aż do samego backendu.

nginx przyjmuje też dyrektywy, które w danym bloku nie mają żadnego działania. Wpis proxy_read_timeout 300s; w bloku PHP z fastcgi_pass jest składniowo bez zarzutu, nginx -t zgłasza syntax is ok, a strona dalej przerywa po 60 sekundach. W drugą stronę tak samo. To najczęstsza przyczyna tego, że podniesiony limit pozostaje bez efektu. Co obowiązuje naprawdę, pokazuje złożona konfiguracja:

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

Jeśli pojedyncza ścieżka naprawdę musi działać dłużej, ustaw limit dokładnie tam i nigdzie indziej. Znak równości robi z tego dopasowanie dokładne, a ono wygrywa z ogólnym blokiem location ~ \.php$, który obsługuje pozostałe pliki 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: dlaczego max_execution_time rzadko tu działa

Nasuwające się przypuszczenie brzmi tak, że PHP samo zakończy zawieszony skrypt. Akurat w tym przypadku najczęściej się to nie sprawdza. Najpierw pułapka pomiarowa: php -i odpytuje wariant z wiersza poleceń. Ten korzysta z własnej konfiguracji w /etc/php/8.4/cli/ i tak czy inaczej działa bez ograniczenia czasu, o FPM nie mówi więc nic. Miarodajne są te dwa miejsca:

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

W pliku puli wartość może być nadpisana przez php_value[max_execution_time] albo php_admin_value[max_execution_time]. Wartości ustawionej przez php_admin_value nie da się już zmienić z poziomu aplikacji przez ini_set(). Jeśli twój framework sam podnosi czas działania, a nagle jest to bez efektu, przyczyna leży właśnie tutaj.

A teraz sedno sprawy: w Linuksie czas oczekiwania w wywołaniach systemowych się nie liczy. Zegar tyka tylko wtedy, gdy skrypt sam liczy. Kiedy czeka na zapytanie do bazy danych, na zewnętrzne API albo na system plików, zegar stoi. Skrypt może więc wisieć dziesięć minut na zablokowanym zapytaniu, a ograniczenie czasu działania nigdy nie zadziała. Kończy go dopiero limit nginx, a wynikiem jest błąd 504.

Wynika stąd niewygodna zasada: max_execution_time chroni przed nieskończonymi pętlami we własnym kodzie, a nie przed czekaniem. Jedyną twardą granicą po stronie PHP jest request_terminate_timeout w pliku puli, która sprząta proces roboczy niezależnie od tego, na czym ten wisi. Daje jednak błąd 502, a nie 504. Limity ustawiaj rosnąco od środka na zewnątrz, żeby jako pierwsza zadziałała ta warstwa, która potrafi jeszcze wygenerować zrozumiały komunikat. Dla skryptów liczących to działa, dla czekających z wymienionego przed chwilą powodu nie. Tam zostaje tylko ograniczenie samego czasu oczekiwania.

Dlaczego podniesienie limitu to najczęściej zła odpowiedź

Policz szybko razem ze mną. Pula z pm.max_children = 10 ma dziesięć procesów roboczych. Strona potrzebuje 90 sekund. Dziesięć równoczesnych wywołań zajmuje więc przez półtorej minuty każdy z nich. W tym czasie nikt nie dostanie już żadnej strony PHP, nawet strony startowej. Z wolnej podstrony zrobiła się awaria. Wzmacniają to trzy mechanizmy:

  • Odwiedzający odświeża stronę. To tworzy dodatkowe żądanie, ale nie zwalnia starego. PHP zauważa przerwanie po stronie odwiedzającego dopiero wtedy, gdy skrypt następnym razem coś wypisze, a skrypt, który liczy, długo nie wypisuje niczego.
  • nginx ponawia sam z siebie. Przy bloku upstream z kilkoma celami proxy_next_upstream stoi domyślnie na error timeout. Żądanie po upływie limitu idzie do następnego serwera, a kosztowne zapytanie wykonuje się po raz drugi. Żądania zapisujące są z tego wyłączone, odczytujące nie. Wyłączysz to przez proxy_next_upstream error;.
  • Monitoring też ponawia. Interwał sprawdzania co 60 sekund na stronie, która potrzebuje 90 sekund, tworzy stałe obciążenie, którego nigdy nie da się nadrobić.

Do tego dochodzi fakt, że nikt nie czeka pięciu minut na stronę internetową. Limit 300 sekund robi z problemu jednominutowego problem pięciominutowy, a odwiedzającego dawno już nie ma, podczas gdy proces roboczy dalej liczy.

Jak bardzo jest zapchane naprawdę, pokazuje strona statusu PHP-FPM. W pliku puli ustaw pm.status_path = /fpm-status, a w bloku server utwórz dostęp osiągalny wyłącznie lokalnie:

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

Zwróć uwagę na active processes, listen queue oraz max active processes. Jeśli listen queue stale utrzymuje się powyżej zera, liczba procesów roboczych nie wystarcza przy obecnym czasie działania stron. To moment, w którym trzeba skrócić czas działania, a nie podnosić limit.

Trzy typowe pożeracze czasu

Baza danych

W większości przypadków czas siedzi właśnie tutaj. Sprawdź najpierw, co akurat działa. We wszystkich czterech systemach dostęp roota idzie domyślnie przez socket uniksowy, polecenie obejdzie się więc bez hasła:

mysql -e "SHOW FULL PROCESSLIST;"

Ciekawe są kolumny TimeState. Wartości w rodzaju Sending data albo Waiting for table metadata lock przy dwucyfrowej liczbie sekund to twój przypadek. Systematycznie szukasz przez log wolnych zapytań, który da się włączyć w trakcie pracy:

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

Nazwę pliku odczytasz z drugiego wyniku, bo bywa różna: Debian stawia zwykle na MariaDB i pisze do /var/log/mysql/mariadb-slow.log, a w Ubuntu plik nazywa się inaczej, zależnie od zainstalowanego serwera. Po kilku minutach przeanalizuj wynik i wyłącz log z powrotem, bo kosztuje on obciążenie zapisem:

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

Przełącznik -s t sortuje po łącznym czasie. Najdroższe zapytanie sprawdzisz przez EXPLAIN, najczęściej brakuje indeksu dokładnie na tej kolumnie, po której odbywa się filtrowanie albo sortowanie. SET GLOBAL działa natychmiast, ale nie przeżywa restartu bazy danych. Górny limit na pojedyncze zapytanie istnieje również po stronie bazy: MariaDB zna max_statement_time w sekundach, MySQL max_execution_time w milisekundach, przy czym to drugie tylko dla zapytań odczytujących. Dzięki temu twoja aplikacja dostaje czysty błąd zamiast zajętego procesu roboczego.

Zewnętrzne interfejsy API

Jeśli twoja strona przy każdym wywołaniu odpytuje operatora płatności albo serwer licencji, jego awaria staje się twoim błędem 504. Zmierz to wywołanie osobno, z poziomu serwera:

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

Jeśli już dns rzuca się w oczy, winne jest rozwiązywanie nazw, a nie druga strona, próba kontrolna przez time getent hosts api.example.com. W kodzie każde zewnętrzne wywołanie potrzebuje własnego, krótkiego limitu: przy cURL są to CURLOPT_CONNECTTIMEOUT oraz CURLOPT_TIMEOUT. Kto zamiast tego wywołuje file_get_contents() na adresie URL, ląduje przy default_socket_timeout z pliku php.ini, gdzie domyślnie stoi 60 sekund:

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

Zasada praktyczna: suma wszystkich zewnętrznych limitów w obrębie jednego żądania musi pozostać poniżej fastcgi_read_timeout. Inaczej nginx utnie połączenie, zanim twój kod zdąży wypisać zrozumiałą stronę błędu.

System plików i pamięć RAM

Pełny dysk spowalnia zapisy albo je uniemożliwia, a dotyka to jednocześnie plików sesji, cache'u i logów. Sprawdź jedno i drugie, wolne miejsce oraz inody:

df -h
df -i

Drugie polecenie bywa zapominane najczęściej. Partycja może być zajęta w 40 procentach i mimo to nie przyjmie już żadnego pliku, gdy inody się wyczerpią. Drugi kandydat to brak pamięci: gdy system zaczyna odkładać dane do swapu, każde żądanie robi się ociężałe, choć żadne konkretne zapytanie nie ponosi winy.

vmstat 1 5

Jeśli kolumny siso stale stoją powyżej zera, kernel bez przerwy odkłada dane do swapu i wciąga je z powrotem, a ty masz problem z pamięcią, a nie z czasem. Jak sobie z tym czysto poradzić, opisuje konfiguracja swapu i unikanie błędu Out-of-Memory. Trzeci kandydat to dyski sieciowe: zawieszony punkt montowania NFS blokuje każdy proces, który go dotknie, a należy do nich również twoje polecenie diagnostyczne. Dlatego postaw przed nim limit czasu:

timeout 5 df -h

Długie zadania nie należą do żądania

Niektóre zadania po prostu trwają: raport roczny, import z 200 000 wierszy, konwersja obrazów. Błąd nie polega na tym, że trwają długo, tylko na tym, że czeka na nie serwer WWW. Czysta forma ma trzy części:

  1. Żądanie zakłada zlecenie, w tabeli albo w kolejce, i odpowiada natychmiast. Pasujący kod statusu to 202, razem z adresem, pod którym da się sprawdzić stan.
  2. Proces roboczy poza nginx pobiera zlecenia i je wykonuje. Nie ma limitu na karku, bo nikt na niego nie czeka.
  3. Interfejs odpytuje o stan. To zapytanie jest zawsze szybkie, niezależnie od tego, jak długo trwa samo zadanie.

Proces roboczy prowadź jako osobną usługę, żeby wracał sam po awarii i po restarcie. Jak wygląda taka jednostka, pokazuje tworzenie usługi systemd. Do zadań w stałych odstępach wystarczy cronjob, tam potrzebujesz blokady, żeby dwa uruchomienia się nie wyprzedziły. Z -n drugie uruchomienie przerywa natychmiast, zamiast czekać:

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

Popularne frameworki mają kolejkę gotową, trzeba ją tylko uruchomić: w Laravelu php artisan queue:work, w Symfony php bin/console messenger:consume z nazwą twojego transportu. Jedno i drugie należy do jednostki systemd, a nie do okna terminala.

Jeden szczególny przypadek zasługuje na wyraźną wzmiankę: WordPress domyślnie uruchamia zaplanowane zadania w obrębie żądań odwiedzających. Odwiedzający płaci więc swoim czasem oczekiwania za to, że w tle leci sprawdzanie aktualizacji. Wiersz define('DISABLE_WP_CRON', true); w pliku wp-config.php, powyżej odwołania do wp-settings.php, to wyłącza. Potem sam regularnie wywołujesz zadania, których termin nadszedł:

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

To, co koniecznie musi pozostać synchroniczne, dostaje własny blok location z własnym limitem, a dodatkowo ograniczenie, żeby ta jedna ścieżka nie zajęła wszystkich procesów roboczych: limit_conn_zone $binary_remote_addr zone=export:10m; w bloku http oraz limit_conn export 1; w odpowiednim bloku. Kolejne próby dostaną wtedy kod 503 zamiast zajętego serwera.

Częste błędy i rozwiązania

Komunikat w oryginalnym brzmieniuZnaczenie i sposób naprawy
upstream timed out (110: Connection timed out) while reading response header from upstreamPrzypadek typowy. Backend liczy za długo. Przeanalizuj log czasów i ustal pożeracza czasu, zanim zaczniesz kręcić limitem.
upstream timed out (110: Connection timed out) while connecting to upstreamNawiązanie połączenia wpadło w limit. Przy zdalnym backendzie prawie zawsze filtr pakietów, który porzuca zamiast odrzucać, przy lokalnym PHP-FPM pełna kolejka na sockecie.
upstream timed out (110: Connection timed out) while reading upstreamNagłówki przyszły, potem korpus zaciął się na dłużej niż limit. Typowe dla eksportów, które w międzyczasie długo liczą.
nginx: [emerg] "fastcgi_read_timeout" directive is not allowed here in /etc/nginx/nginx.conf:12Dyrektywa stoi poza blokiem http, server albo location, najczęściej przez przypadek na samej górze pliku.
nginx: [emerg] unknown directive "proxy_read_timout" in /etc/nginx/sites-enabled/example.com:31Literówka. nginx sprawdza nazwy, a nie intencje. Numer wiersza stoi w komunikacie.
PHP Fatal error: Maximum execution time of 30 seconds exceeded in /var/www/html/export.php on line 42Tutaj wyjątkowo zadziałało ograniczenie czasu działania PHP, skrypt liczył więc, a nie czekał. Daje kod 500 albo pustą stronę, a nie 504.
SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transactionInna transakcja trzyma ten wiersz, limit wynosi domyślnie 50 sekund. Przyczyną jest prawie zawsze transakcja otwarta zbyt długo.
cURL error 28: Operation timed out after 60000 millisecondsZewnętrzne API nie odpowiada. Ustaw w kodzie własny, krótszy limit, żeby twoja aplikacja zachowała kontrolę.
504 w przeglądarce, ale wyszukiwanie upstream timed out nic nie znajdujeTen 504 nie pochodzi z tego nginx, tylko z usługi ustawionej przed nim, na przykład load balancera albo drugiego proxy. Niektóre zgłaszają w takim wypadku własny kod statusu.
Przerwanie dalej po dokładnie 60 sekundach, mimo że limit stoi na 300Zmieniona dyrektywa należy do niewłaściwego modułu albo wygrywa inny blok. nginx -T pokazuje, co obowiązuje naprawdę.

Po czym poznasz, że problem jest naprawiony

Polecenie bez komunikatu o błędzie niczego nie dowodzi, pojedyncze udane wywołanie też nie. Cztery dowody, które razem się bronią:

  1. Kod statusu i czas trwania mierzone bezpośrednio na serwerze, żeby żaden cache nie upiększył wyniku. Spodziewany wynik to kod 200 i czas wyraźnie poniżej limitu. Kod 200 po 58 sekundach przy limicie 60 to nie sukces, tylko kolejna awaria przy odrobinę większym obciążeniu. Liczy się przy tym najgorsze z dwudziestu wywołań:
    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. Log błędów milczy. Przed testem opróżnij go poleceniem truncate -s 0 /var/log/nginx/error.log, wywołaj żądania, a potem zajrzyj do niego ponownie. Pusty plik to właściwy dowód.
  3. Kolejka PHP-FPM stoi na zerze. Dopóki na stronie statusu pod listen queue coś czeka, przyczyna została tylko przesunięta.
  4. Restart niczego nie zmienia. Punkt pomijany najczęściej. Wartości z SET GLOBAL, procesy uruchomione ręcznie i katalogi utworzone samodzielnie poniżej /run tego nie przeżyją. Sprawdź systemctl is-enabled nginx php8.4-fpm i zrestartuj serwer raz w kontrolowany sposób, dopóki jeszcze na to patrzysz.

Ten restart razem z dostępem do konsoli wykonasz przy serwerach root KVM i serwerach dedykowanych od KernelHost w panelu klienta, nawet jeśli usługa WWW akurat niczego już nie wydaje. Serwery stoją w centrum danych maincubes we Frankfurcie nad Menem (certyfikat TÜV TIER3+), a ruch przechodzi przez filtrowanie w sieci przed nimi.

Krótka lista kontrolna na sytuację awaryjną

  1. Policz kody statusu w logu dostępu. Kody 504, 499 i 502 zestawione obok siebie mówią więcej niż każdy z osobna.
  2. grep "upstream timed out" /var/log/nginx/error.log, zanotuj fazę podaną w komunikacie.
  3. Włącz log czasów, porównaj uct, uhturt. Jeśli czas co do sekundy pasuje do skonfigurowanego limitu, zadziałał limit.
  4. Ustal pożeracza czasu: baza danych, zewnętrzne API, system plików albo pamięć.
  5. Poleceniem nginx -T sprawdź, który limit obowiązuje w tym bloku, zanim któryś zmienisz.
  6. Dopiero potem decyduj: naprawić, przenieść do kolejki albo, w ostateczności, podnieść limit dokładnie dla tej jednej ścieżki.
  7. Po poprawce: opróżnij log, zmierz dwadzieścia wywołań, raz w kontrolowany sposób zrestartuj serwer.

Najczęstsze pytania

Jaka jest różnica między 502 Bad Gateway a 504 Gateway Time-out?
Przy 502 połączenie z backendem w ogóle nie dochodzi do skutku albo się zrywa, backend odpowiada więc błędnie lub wcale. Przy 504 połączenie stało, tylko odpowiedź nie przyszła w wyznaczonym czasie: backend był przez cały czas osiągalny i odpowiedział za wolno. Kod 500 oznacza z kolei, że backend odpowiedział, a odpowiedzią był błąd. Stąd wynika kierunek poszukiwań: przy 502 sprawdzasz usługę, socket i uprawnienia, przy 504 czas działania w backendzie.
Ustawiłem fastcgi_read_timeout na 300 sekund, a strona i tak przerywa po 60 sekundach. Z czego to wynika?
Prawie zawsze stąd, że zmieniona dyrektywa należy do niewłaściwego modułu albo wygrywa inny blok. nginx przyjmuje też dyrektywy, które w danym bloku nie mają żadnego działania: wpis proxy_read_timeout 300s; w bloku PHP z fastcgi_pass jest składniowo bez zarzutu, nginx -t zgłasza "syntax is ok", a strona dalej przerywa po 60 sekundach. W drugą stronę tak samo. Co obowiązuje naprawdę, pokazuje złożona konfiguracja przez nginx -T wraz z wyszukaniem read_timeout, send_timeout, fastcgi_pass i proxy_pass. Jeśli pojedyncza ścieżka musi działać dłużej, ustaw limit we własnym bloku location ze znakiem równości, bo dopasowanie dokładne wygrywa z ogólnym blokiem PHP.
Czy wyższy limit rozwiązuje problem?
Najczęściej tylko go przesuwa. Pula z pm.max_children = 10 ma dziesięć procesów roboczych. Jeśli strona potrzebuje 90 sekund, dziesięć równoczesnych wywołań zajmuje przez półtorej minuty każdy z nich, a w tym czasie nikt nie dostanie już żadnej strony PHP, nawet strony startowej. Z wolnej podstrony zrobiła się awaria. Do tego nikt nie czeka pięciu minut na stronę internetową. Jeśli po podniesieniu limitu błąd 504 znika, a w logu dostępu pojawiają się zamiast niego kody 499, to odwiedzający przerwał sam i nic nie zostało rozwiązane.
Który wiersz w logu błędów należy do błędu 504?
Błąd 504 zawsze zostawia wiersz w postaci "upstream timed out (110: Connection timed out) while reading response header from upstream", do znalezienia przez grep -n "upstream timed out" /var/log/nginx/error.log. Decydujący nie jest numer błędu 110, który brzmi tak samo przy każdym 504, tylko faza wymieniona za nim: "while connecting to upstream" oznacza nawiązanie połączenia, które nie doszło do skutku, "while sending request to upstream" to, że nginx nie zdołał przekazać dalej treści żądania, "while reading response header from upstream" jest przypadkiem typowym, w którym backend liczy, nie wysyłając nawet pierwszego wiersza nagłówka, a "while reading upstream" oznacza, że po nagłówkach zaciął się korpus odpowiedzi.
Dlaczego max_execution_time nie kończy mojego zawieszonego skryptu PHP?
Bo w Linuksie czas oczekiwania w wywołaniach systemowych się nie liczy. Zegar tyka tylko wtedy, gdy skrypt sam liczy. Kiedy czeka na zapytanie do bazy danych, na zewnętrzne API albo na system plików, zegar stoi, a skrypt może wisieć dziesięć minut na zablokowanym zapytaniu, choć ograniczenie czasu działania nigdy nie zadziała. Kończy go dopiero limit nginx, a wynikiem jest błąd 504. Uważaj poza tym na pułapkę pomiarową: php -i odpytuje wariant z wiersza poleceń, który korzysta z własnej konfiguracji i tak czy inaczej działa bez ograniczenia czasu. Miarodajne są php.ini od FPM oraz plik puli. Jedyną twardą granicą po stronie PHP jest request_terminate_timeout, ta daje jednak błąd 502, a nie 504.
Jak poznam, czy zadziałał limit, czy backend poddał się sam z siebie?
Po zmierzonym czasie. Zapisuj przez własny log_format wartości $request_time, $upstream_connect_time, $upstream_header_time oraz $upstream_response_time do dodatkowego pliku. Jeśli czas co do sekundy odpowiada skonfigurowanej wartości, na przykład 60,001 sekundy przy limicie 60, to zadziałał limit, a backend nie poddał się sam z siebie. Niepełne wartości w rodzaju 43,7 sekundy pokazują, że hamowało coś innego. Kilka wartości rozdzielonych przecinkiem w jednym polu oznacza ponowienie do drugiego celu, a myślnik zamiast liczby znaczy, że żaden backend nie brał w tym udziału.
Przeglądarka pokazuje 504, ale wyszukiwanie "upstream timed out" nic nie znajduje. Skąd bierze się ten błąd?
Wtedy ten 504 nie pochodzi z tego nginx, tylko z usługi ustawionej przed nim, na przykład load balancera albo drugiego proxy, a niektóre zgłaszają w takim wypadku własny kod statusu. Wcześniej sprawdź jednak jeszcze jedno: wiele wirtualnych hostów pisze do własnego logu błędów, możliwe więc, że szukasz po prostu w niewłaściwym pliku.
Co zrobić z zadaniami, które z natury trwają dłużej niż każdy sensowny limit?
Wyjmij je z żądania. Żądanie zakłada zlecenie i odpowiada natychmiast, odpowiednio z kodem statusu 202 i adresem, pod którym da się sprawdzić stan. Proces roboczy poza nginx wykonuje zlecenie bez limitu na karku, a interfejs odpytuje już tylko o stan. Prowadź ten proces jako osobną usługę, żeby wracał sam po awarii i po restarcie, a powtarzające się przebiegi zabezpiecz przez flock -n, żeby dwa uruchomienia się nie wyprzedziły. To, co koniecznie musi pozostać synchroniczne, dostaje własny blok location z własnym limitem, a dodatkowo ograniczenie przez limit_conn, żeby ta jedna ścieżka nie zajęła wszystkich procesów roboczych.

nginx PHP-FPM 504 Gateway Time-out Timeout Debian Ubuntu Rozwiązywanie problemów Administracja Linuksem