The problem
Acme Shop serves its storefront at shop.example: JavaScript, CSS and images that only change with a deploy, and an API that runs on three app servers. Every browser and every copy of the mobile app connects over HTTPS, so something has to hold the certificate and do the TLS handshakes. Something also has to spread API calls over the three servers and stop sending them to one that fails, answer the same popular product request without asking an app server each time, and slow down a scraper before it slows down everyone else. Building all of that into each app server repeats the work three times and puts the apps directly on the internet.
NGINX does these jobs in one process in front of the apps: it is a web server, a reverse proxy, a load balancer and a content cache. NGINX Open Source is distributed under the 2-clause BSD licence; F5 sells a commercial edition, NGINX Plus, with extra modules and support. Versions on this page are those of October 2026: the stable branch is 1.30 (1.30.5, released on 15 September 2026) and the mainline branch, where new features arrive first, is 1.31 (1.31.6, the same day).
How it works
One master, several workers. The master process reads and checks nginx.conf, opens the listening sockets and starts the worker processes; it never handles a request itself. worker_processes auto starts one worker per CPU core (the built-in default is a single worker). The workers share the listening sockets and accept connections directly. Each worker is single-threaded and runs one event loop over all of its connections with the operating system's readiness API (epoll on Linux, kqueue on FreeBSD and macOS): it serves whichever socket is ready, moves that request one step on, and never sits waiting on a slow client or a slow upstream. A connection costs a file descriptor and some memory, not a thread; nginx.org puts 10,000 idle keep-alive connections at about 2.5 MB. worker_connections caps the connections of one worker (512 by default), upstream connections included, and the process's open-file limit caps it again. When a cache is configured, a cache loader and a cache manager process run as well.
Contexts. nginx.conf is built from simple directives (name value;) and block directives (name { … }) that nest into contexts. The main context holds worker_processes and the events and http blocks; http holds upstream groups and server blocks, one per virtual server; a server holds location blocks, which can nest. Most directives are inherited from the enclosing context unless the inner one sets them, and some only when the inner level sets none of that kind at all: one proxy_set_header in a location drops every proxy_set_header it would otherwise inherit.
Which server, which location. NGINX first picks a server by listening address and port, then by the Host header, and falls back to the default_server of that port. Inside it, the location is chosen by the request URI:
- An exact match (
location = /) ends the search. - Otherwise the longest matching prefix is remembered; if it is marked
^~, the search ends there. - Regular-expression locations (
~,~*) are tried in the order they appear, and the first match wins. - Predicate locations (
location $variable, new in mainline 1.31.5) are tried next. - If nothing else matched, the remembered prefix is used.
So GET /api/products/42 lands in location /api/products/ rather than /api/: both prefixes match and the longer one wins.
Load balancing. An upstream block names a group of servers, and proxy_pass http://catalog sends requests to it.
| Method | How the server is picked | Notes |
|---|---|---|
| weighted round robin | in turn, by weight |
the default |
least_conn |
fewest active connections, by weight | suits requests of uneven length |
ip_hash |
hash of the client address (first three octets of IPv4) | a client keeps its server |
hash key [consistent] |
hash of any key, such as $request_uri |
consistent (ketama) remaps only a few keys when a server is added or removed |
random [two [least_conn]] |
random by weight; two draws two and takes the less busy |
since 1.15.1 |
least_time |
lowest average response time and fewest active connections | open source since mainline 1.31.0; NGINX Plus only before |
Session affinity (the sticky directive with its cookie, route and learn methods, plus the route and drain server parameters) moved from NGINX Plus to open source in 1.29.6, so 1.30 has it.
Passive health checks and retries. Open-source NGINX learns that a server is failing from real traffic. With max_fails=3 fail_timeout=10s, three failed attempts within 10 seconds make a server unavailable for the next 10 seconds (the defaults are 1 and 10 s). proxy_next_upstream defines both what counts as a failure and when to try the next server. Connection errors, timeouts and invalid responses always count; 500, 502, 503, 504 and 429 count only if listed; 403 and 404 never do. By default a request is passed to the next server after an error or a timeout, but only while nothing has been sent to the client yet, and since 1.9.13 a POST, LOCK or PATCH that already reached an upstream is not resent unless you add non_idempotent. proxy_next_upstream_tries and proxy_next_upstream_timeout bound the retries. A group with a single server never marks it unavailable.
Keep-alive to the upstream. Since 1.29.7, and so in 1.30, each worker keeps up to 32 idle connections per upstream group by default (keepalive 32 local) and talks HTTP/1.1 to it (proxy_http_version 1.1). Before, you had to configure both and clear the Connection header yourself. Reusing a connection saves a TCP handshake, and a TLS handshake when the upstream is HTTPS, on every request. The proxy module can also speak HTTP/2 to upstreams since 1.29.4. By default (proxy_buffering on) NGINX reads the upstream's response into buffers as fast as it comes, spilling to a temporary file if needed, so the app server is free again while a slow client is still downloading.
TLS, HTTP/2 and HTTP/3. listen 443 ssl with ssl_certificate and ssl_certificate_key makes NGINX the TLS endpoint: the app servers see plain HTTP, or a new TLS connection if proxy_pass https://… re-encrypts. Since 1.27.3 only TLS 1.2 and 1.3 are enabled by default. HTTP/2 is switched on per server with http2 on; (since 1.25.1; the http2 parameter of listen is deprecated). HTTP/3 over QUIC has been available since 1.25.0 with listen 443 quic reuseport; nginx.org still calls the ngx_http_v3_module experimental. It is not built from source by default (--with-http_v3_module), but nginx.org's Linux packages include it, and OpenSSL 3.5.1 or newer is recommended (older OpenSSL falls back to a compatibility layer without 0-RTT). ssl_verify_client asks clients for certificates, for mutual TLS. The separate ngx_http_acme_module (nginx/nginx-acme) obtains and renews certificates over ACME.
Proxy caching. proxy_cache_path declares a cache. Responses are stored as files on disk, named by the MD5 of the cache key, while the keys and their metadata sit in a shared memory zone that every worker reads (about 8,000 keys per megabyte). proxy_cache switches it on for a location and proxy_cache_valid 200 60s keeps successful responses for 60 seconds. The default key is $scheme$proxy_host$request_uri, and only GET and HEAD responses are cached unless proxy_cache_methods says otherwise. The upstream can set the lifetime itself with X-Accel-Expires, Expires or Cache-Control, which take precedence over proxy_cache_valid; a response with Set-Cookie is not cached, and Vary splits the entry by request header. $upstream_cache_status reports MISS, BYPASS, EXPIRED, STALE, UPDATING, REVALIDATED or HIT. proxy_cache_lock on lets one request fill a missing entry while the others wait for it, and proxy_cache_use_stale error timeout updating serves the old copy while the upstream is down or being asked for a new one.
Rate and connection limits. limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s keeps one state per client address in shared memory; a state takes 128 bytes on 64-bit platforms, so a megabyte holds about 8,000 addresses. limit_req zone=perip burst=20 lets requests above 10 a second wait in a queue of up to 20 and releases them at the configured rate (the module describes this as a leaky bucket). Past that, the request is rejected with 503, or with the status set by limit_req_status, 429 here. nodelay serves the burst at once instead of spacing it out, and delay= mixes the two; limit_req_dry_run on only counts. limit_conn caps concurrent connections per key in the same way. Each zone belongs to one NGINX instance: two instances count separately, and synchronising zones across a cluster (sync) is an NGINX Plus feature.
Static files and compression. root /var/www maps /static/app.js to /var/www/static/app.js; alias replaces the location's prefix instead, and try_files tries a list of paths before falling back. sendfile on (off by default) lets the kernel copy a file to the socket. Compression is off until gzip on, and gzip_types lists only text/html by default, so add JSON, CSS and JavaScript; gzip_static serves files compressed at build time.
WebSockets. Upgrade and Connection are hop-by-hop headers, so a location that proxies WebSockets passes them on explicitly (proxy_set_header Upgrade $http_upgrade; and proxy_set_header Connection "upgrade";). When the upstream answers 101, NGINX turns the request into a two-way tunnel; this has worked since 1.3.13.
Health checks: open source and NGINX Plus. Open source has the passive checks above, which need real requests to notice a failure. NGINX Plus adds active checks (health_check, by default every 5 seconds), a slow_start ramp for a recovered server, an API that adds and removes upstream servers without a reload, and a status dashboard. Open source does offer stub_status for basic counters (active, reading, writing and waiting connections, accepted and handled connections, requests).
Reload and binary upgrade. nginx -s reload sends HUP to the master. The master checks the new configuration and opens any new log files and listening sockets; if that fails it keeps running the old configuration. If it succeeds, it starts new workers with the new configuration and asks the old workers to shut down gracefully: they close their listening sockets, finish the requests they hold and exit. Long-lived connections such as WebSockets keep an old worker alive until they end, unless worker_shutdown_timeout closes them. Replacing the binary itself works the same way: USR2 starts a new master with the new executable next to the old one, WINCH tells the old workers to finish, and QUIT retires the old master once the new one is trusted. Mainline 1.31.5 adds a Control API, a REST interface in the master (built with --with-control-api, started with nginx -l unix:/path/to.sock) that lists worker processes, returns the loaded configuration and triggers reloads; it does not change upstreams.
In Kubernetes. Two different projects carry the name. The community ingress-nginx controller, maintained under Kubernetes SIG Network, was retired: maintenance ended in March 2026, its repository was archived on 24 March 2026, and it gets no further releases or security fixes. Existing installations keep running, and Kubernetes SIG Network recommends moving to the Gateway API or another ingress controller. F5 NGINX Ingress Controller (nginx/kubernetes-ingress, Apache 2.0) is a separate controller made by F5 that runs NGINX Open Source or NGINX Plus, and NGINX Gateway Fabric implements the Gateway API with NGINX as the data plane.
Licence and forks. NGINX Open Source is under the 2-clause BSD licence and its source lives on GitHub (nginx/nginx). NGINX Plus is commercial. freenginx is a fork that describes itself as an effort to keep nginx's development free and open, under the same BSD-style licence.
Where it fits
- Solutions: the front door of websites and APIs (TLS, static files, proxying to app servers), an origin cache behind a CDN, an internal load balancer between tiers, a reverse proxy in front of PHP-FPM, uWSGI or gRPC services, and the data plane of Kubernetes ingress and Gateway API controllers.
- Patterns in this catalog it implements or supports: Load Balancing with
upstreamgroups and passive checks; Gateway Offloading of TLS, compression and caching; Rate Limiting withlimit_reqandlimit_conn; the origin tier of CDN & Edge Caching, or a small self-run cache tier; Mutual TLS withssl_verify_client; Health Endpoint Monitoring, passively in open source and with active probes of a health URL in NGINX Plus; Blue-Green Deployment and Canary Release by switching or weighting upstream servers and reloading; and a Strangler Fig migration that routes one path at a time to the new system. It can act as a simple API Gateway for routing and limits. In Kubernetes an ingress or Gateway API controller plays this role. - Usual neighbours: browsers, mobile apps and CDNs in front; often a cloud network load balancer in front of two or more NGINX instances for availability; app servers, PHP-FPM and gRPC services behind; certificates from an ACME CA or a secrets store; access logs shipped to a log pipeline and
stub_statusscraped for metrics. - Managed offerings: AWS does not run NGINX as a managed service of its own. You run it yourself on EC2, ECS or EKS, or buy NGINX Plus as an AMI through AWS Marketplace. The managed AWS services for the same jobs are the Application Load Balancer (layer-7 routing, health checks, TLS with certificates from AWS Certificate Manager), CloudFront (caching at the edge), AWS WAF rate-based rules (rate limiting) and Amazon API Gateway (throttling and API management). On Azure, F5 NGINXaaS for Azure is a managed service built on F5's commercial NGINX.
When to use it
Use NGINX when one process should do the edge work for a site or an API that you run yourself: TLS, static files, caching, rate limits and load balancing across a fixed or slowly changing set of servers. Choose something else when upstreams change by the minute and you need an API instead of reloads, when you want active health checks without paying for NGINX Plus, or when you would rather not run the edge at all.
| NGINX Open Source | HAProxy | Envoy | Caddy, Traefik | AWS ALB | |
|---|---|---|---|---|---|
| What it is | web server, reverse proxy, load balancer and cache | reverse proxy and load balancer for TCP and HTTP | layer-7 proxy designed for large service-oriented systems and service meshes | Caddy: web server; Traefik: reverse proxy that configures itself | managed layer-7 load balancer |
| Changing upstreams | edit the file and reload; DNS with resolve; an API in NGINX Plus |
runtime API adds and removes servers, and since 3.4 whole backends, without a reload | dynamic discovery APIs (xDS) from a control plane | Caddy: JSON admin API; Traefik: follows Docker, Kubernetes and other providers | API or console; targets registered by Auto Scaling or ECS |
| Health checks | passive (active in NGINX Plus) | active checks | active checks plus outlier detection | active and passive (Caddy) | active, per target group |
| Response cache | built in, on disk | small cache | cache filter | — | none (CloudFront in front) |
| Certificates | files, or the ACME module | files, ACME since 3.2 | files, or pushed by the control plane | automatic HTTPS through ACME | AWS Certificate Manager |
| Licence | 2-clause BSD | GPLv2, LGPL headers | Apache 2.0 | Apache 2.0 (Caddy), MIT (Traefik) | a managed service |
| Choose it when | you want one process for TLS, files, caching and proxying | load balancing is the whole job and you want runtime changes and active checks in open source | you run many services, want dynamic configuration and rich telemetry, often with a mesh | small sites that should get certificates on their own, or containers that come and go | you are on AWS and want it managed, with AWS WAF and ACM |
Trade-offs
- Configuration is static. Changes go through a file and a reload. A reload doesn't drop requests, but frequent reloads leave old workers alive while long connections drain, and every change needs a deploy step. DNS re-resolution (
resolve) covers servers behind a changing name, and the API for live upstream changes is an NGINX Plus feature. - Open-source health checks are passive. A failure is noticed by a real request (retried on another server if it is safe), and a server marked down is tried again by real traffic once
fail_timeoutpasses. Active checks, slow start and the status API are in NGINX Plus. - State is per instance. The cache, the rate-limit zones and the failure counts live in one NGINX. Two instances each allow 10 r/s per client, and each warms its own cache.
- The configuration language has sharp edges. Location matching order, directives that inherit only when the inner level sets none, and the difference between
rootandaliascause many production surprises; test withnginx -tand keep the configuration in version control. - One instance is a single point of failure. Run at least two, behind a cloud load balancer or a floating IP.
- Not a full API gateway. Per-key quotas, request transformation, developer portals and fine-grained authorization need modules, njs scripts or a dedicated gateway; Envoy-based gateways change routes through APIs.
Implementation notes
The scenario's configuration (checked with nginx -t on nginx 1.30.5):
# /etc/nginx/nginx.conf for shop.example (nginx 1.30 stable)
worker_processes auto; # one worker per CPU core; the default is 1
events {
worker_connections 4096; # per worker, upstream connections included; default 512
}
http {
sendfile on; # static files go from disk to socket in the kernel
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
proxy_cache_path /var/cache/nginx/api keys_zone=api_cache:10m
max_size=1g inactive=10m;
upstream catalog {
least_conn;
server app-1:8080 max_fails=3 fail_timeout=10s;
server app-2:8080 max_fails=3 fail_timeout=10s;
server app-3:8080 max_fails=3 fail_timeout=10s;
# keepalive 32 local; is the default since 1.29.7
}
server {
listen 80;
server_name shop.example;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name shop.example;
ssl_certificate /etc/nginx/tls/shop.example.crt;
ssl_certificate_key /etc/nginx/tls/shop.example.key;
location /static/ {
root /var/www; # /static/app.js is /var/www/static/app.js
}
location /api/ {
limit_req zone=perip burst=20; # past 10 r/s, up to 20 wait
limit_req_status 429; # the default is 503
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://catalog;
location /api/products/ { # longest prefix wins
proxy_cache api_cache;
proxy_cache_valid 200 60s;
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://catalog;
}
}
}
}
The nested location /api/products/ inherits the rate limit and the proxy_set_header lines from /api/ but needs its own proxy_pass. Without proxy_set_header Host $host the app servers would see Host: catalog, the name of the upstream group. Upstream host names such as app-1 are resolved once, when the configuration loads, unless the group has a zone, a resolver is set and the servers carry resolve.
- Test, then reload.
nginx -tchecks syntax and files without touching the running server;nginx -s reloadthen applies the change. A failed reload leaves the old configuration running, so check the error log after each one. - Find the real client address. Behind a cloud load balancer every connection comes from the balancer, so
$binary_remote_addrwould put all clients in one rate-limit bucket. The realip module (set_real_ip_fromwith the balancer's addresses, plusreal_ip_header X-Forwarded-Foror the PROXY protocol) restores the client's address. - Log the upstream side. Add
$upstream_addr,$upstream_status,$upstream_response_timeand$upstream_cache_statusto thelog_format: they show which server answered, how long it took, which attempt failed before a retry, and whether the cache helped. - Size the limits.
worker_connectionstimes the number of workers bounds open connections, and each proxied request uses two (client and upstream). Raise the open-file limit (worker_rlimit_nofile) with it. - Plan for long-lived connections. WebSockets and streaming responses keep old workers alive after a reload; set
worker_shutdown_timeoutif reloads are frequent. - Keep it patched. NGINX parses untrusted input on the internet edge, and both branches ship security fixes regularly (several CVEs were fixed in 2026); follow the stable branch unless you need a mainline feature.