What NGINX Does Before Your App Sees a Request (and Where 502 vs 504 Come From)

October 1, 2026 · How It Actually Works: Real Systems as State Machines (part 7)

▶ Watch on YouTube & subscribe to The Stack Underflow

In many deployments NGINX sees every request before your application does. It can answer on its own, reject the request, or pass it to one of several backends, and when it can’t get a usable answer from them, you get a 502 or a 504 that your app never produced. This video draws open-source nginx as one hierarchical state machine, from startup through one request to shutdown.

The one-line version: a timeout becomes 504; a refused connection, an invalid response header, or no live upstream becomes 502. And a reload starts new workers while the old ones finish their requests.

Last verified against the nginx.org documentation, CHANGES and the nginx 1.30 source: 1 October 2026. Scope: open-source nginx, stable 1.30 (1.30.5 at the time of checking), HTTP reverse proxy to an upstream group. Not covered: NGINX Plus features, the stream module, HTTP/3, Kubernetes ingress controllers.

Six memory pegs

From the NGINX words primer, where NGINX is an airport. These are analogies, memory aids only.

WordMemory pegWhat it actually is
masterThe airport managerReads the configuration, opens sockets, manages workers; no client I/O
workersThe air-traffic controllersEvent-driven processes that do the request processing
server blockThe terminal and its signA virtual server, picked by address:port, then Host
locationThe gateConfiguration picked by the request path
upstreamThe connecting fleetA named group of backend servers
504 / 502No answer in time / refused or garbledThe two gateway errors NGINX generates itself

Chapter 1 · Startup

You start NGINX with the nginx command; in a container, the official image runs it in the foreground with daemon off, so the container runtime supervises the master.

  1. Read nginx.conf and every included file. This is the same check nginx -t runs: syntax first, then opening the files the configuration refers to. A syntax error or a file it can’t open is an emergency-level error and NGINX doesn’t start.
  2. Open listen sockets (say 80 and 443) before any worker exists; workers inherit them. If a port is taken, NGINX logs the bind failure, waits 500 ms (“try again to bind() after 500ms”) and retries, five attempts in all, then gives up with “still could not bind()”.
  3. Start workers. worker_processes defaults to 1; auto tries to detect the CPU cores.

Chapter 2 · Reload without dropping requests

nginx -s reload sends HUP to the master. From the docs: the master “first checks the syntax validity, then tries to apply new configuration, that is, to open log files and new listen sockets. If this fails, it rolls back changes and continues to work with old configuration.” On success it starts new workers and asks the old ones to shut down gracefully; the old workers “close listen sockets and continue to service old clients”, then exit. That’s why a reload drops nothing, and why a typo in nginx.conf just leaves the old configuration running.

Other master signals: USR1 reopens log files (log rotation without a restart); QUIT is graceful shutdown; TERM and INT are fast.

Chapter 3 · A connection arrives

Each worker runs an event loop (epoll on Linux) and holds many connections, up to worker_connections (default 512), a count that includes connections to upstream servers.

  • TLS handshake. For HTTPS it happens first. The SNI name lets NGINX choose which server block’s certificate to present. A failed handshake closes the connection before any HTTP is read.
  • Read the request line and headers. Malformed ones get 400 before any of your locations is involved.
  • Choose the server block. Narrow to blocks listening on the connection’s address and port, then match the Host header against server_name: exact name, then the longest wildcard starting with *, then the longest ending with *, then the first matching regex. No match or no Host: the default server for that port, the first one listed unless default_server is set.

Chapter 4 · Eleven phases, a fixed order

The order is fixed by NGINX, not by the order of lines in your configuration:

PhaseWhat happens there
POST_READrealip can replace the client address with one from a trusted proxy
SERVER_REWRITErewrite directives written directly in the server block
FIND_CONFIGpicks the location: = exact stops; longest prefix remembered (^~ skips regex); regexes in file order, first match wins; else the prefix
REWRITErewrite directives inside the chosen location; they can change the URI
POST_REWRITEURI changed? Back to FIND_CONFIG. After 10 changes: 500, “rewrite or internal redirection cycle”
PREACCESSlimit_req, limit_conn; over the limit is rejected, 503 by default
ACCESSallow/deny (403), authentication such as auth_basic (401)
POST_ACCESScombines access results per satisfy all or satisfy any
PRECONTENTtry_files, mirror
CONTENTwho produces the response: proxy_pass, a static file (root/alias, 404 if missing), or return
LOGthe access log line is written

The POST_REWRITE loop is why NGINX configuration is request-processing logic, not a static list of settings. The limit of 10 is NGX_HTTP_MAX_URI_CHANGES in the source.

Chapter 5 · Proxying, retries, and 502 vs 504

A location with proxy_pass sets its own content handler. Proxying is its own small state machine:

  1. Choose a peer. Weighted round robin by default (least_conn, hash, ip_hash, random can be configured). Servers currently marked unavailable are skipped.
  2. Connect or reuse. Since nginx 1.29.7, and so in stable 1.30, the proxy uses HTTP/1.1 with keepalive by default, and the upstream connection cache is on by default. proxy_connect_timeout is 60s.
  3. Send the request, with Host set to $proxy_host by default. Headers such as X-Forwarded-For are added only if you configure proxy_set_header.
  4. Wait for the response header. proxy_read_timeout (60s) “is set only between two successive read operations, not for the transmission of the whole response.”
  5. Body. proxy_buffering is on by default, so a slow client doesn’t hold the backend busy.

When an attempt fails (refused or reset connection, connect timeout, read timeout, invalid header), proxy_next_upstream (default error timeout) decides whether to try the next server. Two limits: it’s only possible “if nothing has been sent to a client yet”, and a POST, LOCK or PATCH that already reached a server isn’t retried unless you add non_idempotent. Passively, with the defaults max_fails=1 and fail_timeout=10s, one failed attempt marks the server unavailable for 10 seconds; active health checks are an NGINX Plus feature. If every server is unavailable: 502, “no live upstreams”.

When NGINX gives up, the status depends on why. The mapping is in ngx_http_upstream_next() in the 1.30.0 source:

case NGX_HTTP_UPSTREAM_FT_TIMEOUT:
case NGX_HTTP_UPSTREAM_FT_HTTP_504:
    status = NGX_HTTP_GATEWAY_TIME_OUT;   /* 504 */
    break;
/* ... */
default:
    status = NGX_HTTP_BAD_GATEWAY;        /* 502 */
What happenedStatus
Connect, send or read timed out504 Gateway Timeout
Connection refused or reset502 Bad Gateway
Invalid response header502
No live upstream servers left502

Either way, your application may never have produced that status, which is why a 504 often comes with nothing in the app’s logs. (The same function has cases that pass through other codes such as 500 or 503 when you add conditions like http_500 to proxy_next_upstream; those are not defaults and the video doesn’t cover them.)

Chapter 6 · Shutting down

QUIT (nginx -s quit) is graceful: each worker closes its listen sockets, finishes the requests it is serving and exits; then the master exits. It mirrors a reload without the new workers. The official Docker image sets STOPSIGNAL SIGQUIT, so docker stop is graceful too. TERM or INT is a fast shutdown, without waiting for requests in progress.

Pause & Prove

1. NGINX returns 504 and your app logs show nothing. Which proxy setting timed out, and would a refused connection have given the same status?

  • proxy_read_timeout, most often. ✓ 60s by default, measured between two reads. Any proxy timeout (connect, send, read) becomes 504.
  • A refused connection gives 504 too. No: a refusal is an error, not a timeout, so it gives 502, unless another server succeeds on retry.
  • The app returned 504. Possible in general, but “nothing in the app logs” points to NGINX generating it.
  • proxy_read_timeout limits the whole response. No: it resets on every read, so a slow but steady response doesn’t hit it.

2. You run nginx -s reload with a typo in nginx.conf. What happens? (Community poll)

  • NGINX stops. No: the master stays up.
  • Workers restart and drop requests. No: no new workers start, and the old ones never stop.
  • The old configuration keeps running. ✓ The master checks the syntax, fails, rolls back, and the old workers keep serving.
  • NGINX starts with defaults. There’s no fallback to defaults; it keeps the last good configuration.

3. Peg drill

Say the peg for each: the master, a location, the upstream, 504. Answers: the airport manager; the gate; the connecting fleet; no answer in time (and 502: refused or garbled).

Before / after this video

Sources

Checked on 1 October 2026, plus the video’s own verified sources:

Change notes

  • 1 Oct 2026: first published. Added a note that non-default proxy_next_upstream conditions such as http_500 can pass other status codes through; the video covers the defaults only.

Not affiliated with or endorsed by F5 or the nginx project. NGINX is a trademark of F5, Inc. Found a mistake? Tell us in the video’s comments and we’ll correct this page.

Found this useful? The deep version lives on YouTube — new breakdowns of how AI dev tools actually work, weekly.

Subscribe on YouTube →

Prefer email? Get the free newsletter: one failure, traced step by step, about once a week.