What NGINX Does Before Your App Sees a Request (and Where 502 vs 504 Come From)
▶ 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.
| Word | Memory peg | What it actually is |
|---|---|---|
| master | The airport manager | Reads the configuration, opens sockets, manages workers; no client I/O |
| workers | The air-traffic controllers | Event-driven processes that do the request processing |
| server block | The terminal and its sign | A virtual server, picked by address:port, then Host |
| location | The gate | Configuration picked by the request path |
| upstream | The connecting fleet | A named group of backend servers |
| 504 / 502 | No answer in time / refused or garbled | The 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.
- Read
nginx.confand every included file. This is the same checknginx -truns: 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. - 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()”.
- Start workers.
worker_processesdefaults to 1;autotries 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 unlessdefault_serveris set.
Chapter 4 · Eleven phases, a fixed order
The order is fixed by NGINX, not by the order of lines in your configuration:
| Phase | What happens there |
|---|---|
| POST_READ | realip can replace the client address with one from a trusted proxy |
| SERVER_REWRITE | rewrite directives written directly in the server block |
| FIND_CONFIG | picks the location: = exact stops; longest prefix remembered (^~ skips regex); regexes in file order, first match wins; else the prefix |
| REWRITE | rewrite directives inside the chosen location; they can change the URI |
| POST_REWRITE | URI changed? Back to FIND_CONFIG. After 10 changes: 500, “rewrite or internal redirection cycle” |
| PREACCESS | limit_req, limit_conn; over the limit is rejected, 503 by default |
| ACCESS | allow/deny (403), authentication such as auth_basic (401) |
| POST_ACCESS | combines access results per satisfy all or satisfy any |
| PRECONTENT | try_files, mirror |
| CONTENT | who produces the response: proxy_pass, a static file (root/alias, 404 if missing), or return |
| LOG | the 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:
- Choose a peer. Weighted round robin by default (
least_conn,hash,ip_hash,randomcan be configured). Servers currently marked unavailable are skipped. - 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_timeoutis 60s. - Send the request, with
Hostset to$proxy_hostby default. Headers such asX-Forwarded-Forare added only if you configureproxy_set_header. - 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.” - Body.
proxy_bufferingis 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 happened | Status |
|---|---|
| Connect, send or read timed out | 504 Gateway Timeout |
| Connection refused or reset | 502 Bad Gateway |
| Invalid response header | 502 |
| No live upstream servers left | 502 |
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_timeoutlimits 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).
Related Shorts
- NGINX 504 Gateway Timeout: what actually timed out
- NGINX 502 Bad Gateway: the backend refused the connection
- What nginx -s reload does with requests in flight
Before / after this video
- Before: NGINX words: worker, server block, location, upstream, proxy_pass, 502 vs 504 and Backend words: port, socket, reverse proxy, JVM, pod, SIGTERM
- The rest of the path: What Apache Tomcat actually does with your request and the Spring Boot lifecycle. Put together: NGINX handles TLS, routing, limits and proxying; Tomcat runs the connector, Coyote, the valves, the filters and the servlet; Spring MVC’s
DispatcherServletfinds your controller. - Next in the series: Spring AI words: ChatClient, advisors, memory, RAG, tools, MCP
Sources
Checked on 1 October 2026, plus the video’s own verified sources:
- nginx download page and CHANGES-1.30: stable 1.30.5; no 1.30.x change to timeouts, retries, 502/504 mapping, reload or worker defaults
- nginx CHANGES: 1.29.7, upstream
keepaliveon by default, proxy keepalive by default,proxy_http_version1.1 - ngx_http_proxy_module: timeouts (60s, between successive reads/writes),
proxy_next_upstreamdefault and retry rules,proxy_http_version,proxy_buffering,Host $proxy_host - ngx_http_upstream_module: weighted round robin,
max_fails,fail_timeout,keepalivedefault since 1.29.7 - Controlling nginx: signal tables, the HUP reload sequence
- Beginner’s Guide and command-line parameters: reload, quit,
-t - Core functionality:
worker_processes,worker_connections - How nginx processes a request and ngx_http_core_module: location: server selection, default server, location order
- Configuring HTTPS servers: handshake before HTTP, SNI
- Development guide: the eleven HTTP phases, master and worker roles
- nginx 1.30.0 source: ngx_http_upstream.c (504/502 mapping, “no live upstreams”), ngx_http_request.h (
NGX_HTTP_MAX_URI_CHANGES10), ngx_connection.c (5 bind tries, 500 ms apart) - docker-nginx Dockerfile:
daemon off,STOPSIGNAL SIGQUIT(checked for the video in September 2026)
Change notes
- 1 Oct 2026: first published. Added a note that non-default
proxy_next_upstreamconditions such ashttp_500can 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.