Nginx is an event-driven HTTP, stream, and mail proxy and server. Treat the configuration, its process privileges, and upstream timeouts as production code: validate changes before reloading and retain a known-good configuration.
Event-Driven I/O #
Nginx is built around nonblocking event loops. A master process reads the configuration and starts workers; each worker then waits on epoll, kqueue, or the platform’s equivalent readiness backend. The worker keeps a connection record for each peer and advances each request only when the socket becomes ready.
One event loop can therefore handle many connections without one thread per request. The model also explains why logging and buffering choices matter: the worker must stay responsive while upstreams, clients, and disk writes progress independently.
Isolated Configuration #
Run an instance with an explicit prefix and configuration when testing a local build. The prefix resolves relative paths such as logs and temporary files.
nginx -p "$PWD" -c nginx.conf
nginx -t -p "$PWD" -c nginx.confFor an unprivileged local process, use a non-privileged listener and set pid,
logs, and each request-body or upstream temporary path to writable locations.
daemon off; and master_process off; simplify foreground debugging but are
not production settings.
The include directive is textual configuration inclusion relative to the
prefix or an absolute path. Validate the complete include graph with nginx -t
and inspect the expanded configuration with nginx -T. On reload, the master
parses and validates the new configuration, starts new workers when successful,
and asks old workers to finish current work before exiting. A parse or resource
acquisition failure leaves the old configuration serving traffic.
Virtual Servers and CORS #
Nginx selects an HTTP virtual server from the listener and Host header.
Within a listener, selection prefers an exact server_name, then the longest
leading wildcard, then the longest trailing wildcard, and finally the first
matching regular expression. server_name .example.com is shorthand for the
apex and *.example.com; *.example.com does not match the apex alone.
After selecting a server, Nginx finds a location from prefix and regular
expression rules. Exact locations win immediately; otherwise Nginx remembers
the longest prefix and evaluates eligible regular expressions in order. The
^~ modifier prevents regular-expression checks after that prefix match.
Internal rewrites can start another location search, so trace the final URI when
proxy behavior differs from the visible request.
CORS must name permitted origins when credentials are allowed. * cannot be
combined with Access-Control-Allow-Credentials: true. Reflect an origin only
after an allow-list match and add Vary: Origin, because shared caches must
not reuse a credentialed response for another origin.
map $http_origin $cors_origin {
default "";
"~^https://(app|admin)\.example\.com$" $http_origin;
}
server {
location /api/ {
if ($cors_origin != "") {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Vary Origin always;
}
proxy_pass http://api_upstream;
}
}Handle preflight OPTIONS requests with the same allow-list and explicit
allowed methods and headers. Do not forward untrusted Host headers to an
upstream that uses them for authorization or URL generation; set Host to the
expected upstream name where required.
Proxied GET Requests #
HTTP defines GET as safe, but an application can still make it expensive.
Content in a GET request has no generally defined semantics, so an upstream
should not depend on a GET body unless a private contract defines and protects
that behavior.
Nginx can retry a proxied request after an upstream failure according to
proxy_next_upstream; a retry can therefore cause the upstream application to
execute the same GET more than once. Keep GET handlers free of side effects,
set an explicit retry policy, and use an idempotency key or a different method
for operations that create work.
An upstream response timeout does not prove that the upstream did nothing. Log request IDs at the proxy and application boundary, then use those IDs to distinguish a retry from a client retry or a duplicated message.
By default, proxying preserves the request method and body while constructing a new upstream request with Nginx-controlled headers. Set authority and forwarding metadata explicitly:
location /api/ {
proxy_set_header Host api.internal.example;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://api_upstream/;
}Because proxy_pass includes a URI here, the normalized part matching
/api/ is replaced with / before forwarding. Without a URI in
proxy_pass, Nginx forwards the request URI according to the documented
rewrite rules. Review this distinction when changing a trailing slash.
Do not convert a mutating request to GET with proxy_method unless the
upstream contract also handles or discards the original request body
deliberately.
Source Architecture #
Nginx starts a master process that reads configuration and manages workers.
Workers use an event mechanism such as epoll or kqueue, maintain connections in
ngx_connection_t, and advance request processing through handler phases.
Modules declare a type—core, event, HTTP, stream, or mail—and register
directives, configuration creation and merge functions, and runtime handlers.
Each module exposes a global ngx_module_t descriptor. The module type tells
Nginx which configuration structures and initialization hooks belong together.
Core source files and extra modules are wired up during the build, while a small
set of modules can be built dynamically and loaded later.
HTTP request processing is organized into phases such as rewrite, access, content, and log. A request moves through those phases after parsing the request line and headers, thus a module can hook one stage without owning the entire request path.
Logging Internals #
ngx_log_error_core() is the core logging function. Higher-level macros such
as ngx_log_error() and ngx_log_debug() wrap it and respect the configured
log level. Debug logging is compiled and enabled only when the build and
runtime configuration allow it.
The error_log directive controls where errors go. Access logs are separate
and are intentionally not treated as the same channel as error logging. In
debug builds, the debug log level makes it practical to trace a request
through configuration parsing, location lookup, phase handlers, and upstream
processing.
Build a debug binary from a pinned source release with --with-debug, run it
with a dedicated prefix, and inspect error logs at the debug level. Start
source reading with configuration parsing and cycle initialization, then follow
an accepted connection through the event loop, HTTP request-line parsing,
location configuration, phase handlers, and upstream processing.
Comments
Read or comment on GitHub (sign-in required to comment)