Nginx Architecture
Nginx is fast because of how it handles concurrency, not because of any single optimization. The model: one master process, a few worker processes, each worker handling thousands of connections via a non-blocking event loop.
Master / worker model
nginx master process ← reads config, manages workers, listens for signals
│
├── worker 1 (single-threaded, event loop, ~10k connections)
├── worker 2
├── worker 3
└── worker 4 ← typically 1 per CPU core
The master:
- Reads and validates config.
- Binds the listening sockets.
- Spawns workers; restarts them if they crash.
- Handles signals (
HUPfor reload,USR2for binary upgrade). - Owns privileges (root for binding port 80) — workers drop to an unprivileged user.
Workers do all the actual request handling. They never share memory with each other (no locking needed); they share the listening sockets and accept incoming connections as they’re available.
Common config:
worker_processes auto; # one per CPU core
worker_connections 1024; # per worker
events {
use epoll; # Linux event mechanism (kqueue on BSD)
multi_accept on; # accept all pending connections in one go
}
worker_processes × worker_connections = upper bound on simultaneous connections (each connection costs 1 fd in worker; reverse-proxied requests cost 2 — client + upstream).
Event loop, not threads
Apache (prefork) used one process per request — fine for a few hundred concurrent connections, breaks at thousands (memory + context-switch overhead).
Nginx uses a single-threaded event loop per worker:
loop:
wait for any of N sockets to be ready (epoll_wait)
for each ready socket:
do the small bit of work that's possible without blocking
(read what's available, write what fits, parse headers, etc.)
A worker juggles ~10k connections concurrently because most are idle most of the time (waiting for the network or upstream). When something is ready to do, the worker does it; when nothing is, the worker sleeps in epoll_wait.
This is the same model as Python’s asyncio, Node.js, Go’s netpoller — all built on the same OS primitives (epoll/kqueue/io_uring).
Why workers are single-threaded
Single-threaded per worker = no locks, no thread-safety overhead, no race conditions. The cost: a single worker pegged at 100% CPU on one core can’t use other cores.
Solution: many workers. With worker_processes 8 on an 8-core machine, you have 8 independent event loops, each on its own core, sharing nothing but the listening sockets.
When workers fight for the accept lock (some kernels), enable accept_mutex off (the default in modern nginx) — kernels handle wake-up balancing fine.
Why nginx scales
Memory per connection: a few KB (just the request state — no per-connection thread stack). 10k connections per worker × 4 KB ≈ 40 MB.
Context switches: minimal — a worker handles many connections in one stack frame.
Compare to Apache prefork: one process per request, ~10 MB RSS each, hard cap around 1000 concurrent.
For Python interviewers: nginx is to Apache what asyncio is to threading.
What nginx is good at
- Static files —
sendfile()syscall puts file → socket without copying through user space. Saturates a 10 Gbps NIC easily. - Reverse proxy — small CPU per request, parallel upstream fan-out.
- TLS termination — even without specific HW acceleration, modern CPUs with AES-NI handle it cheaply at the edge.
- Slow-client buffering — reads slow uploads to its memory, then forwards the whole request to the upstream in one shot, freeing the upstream worker.
What nginx is not good at
- CPU-heavy logic — anything serious goes to upstream workers. Lua/Perl/JS modules exist but for small glue, not business logic.
- Long-running connections under high churn — fine for steady WebSockets, but constant connect/disconnect spikes need tuning.
- Dynamic config without reload — config is read at start/HUP. For dynamic upstreams use OpenResty + Lua, or switch to Envoy/Traefik (built for dynamic config).
Reload vs restart
nginx -s reload # SIGHUP — re-reads config, spawns new workers, drains old ones
nginx -s quit # SIGQUIT — graceful shutdown, finish in-flight, then exit
nginx -s stop # SIGTERM — fast shutdown
nginx -s reopen # SIGUSR1 — reopen log files (use after log rotation)
Reload is online: existing connections finish on the old workers, new connections go to new workers. Zero-downtime config changes.
For binary upgrades (new nginx version), kill -USR2 <master_pid> runs old + new master in parallel; both share the listening sockets via SO_REUSEPORT or socket inheritance. No dropped connections.
Connection lifecycle, simplified
1. epoll_wait: client socket ready
2. accept the connection
3. read request bytes (may take several loops if client is slow)
4. parse request
5. apply config (find matching server / location)
6. either:
- serve static file via sendfile()
- or open upstream socket, forward request
7. wait for upstream response (epoll_wait again)
8. stream/buffer response back to client
9. close or keep-alive for next request
All while the same worker juggles 9999 other connections in similar states.
Common interview confusions
- “More workers = more performance.” — only up to CPU count (or thereabouts). Beyond that, workers fight each other for the same cores; performance plateaus or drops.
- “Nginx is multi-threaded.” — workers are typically single-threaded. Newer nginx supports thread pools for blocking syscalls (file I/O on slow disks), but request handling stays event-driven.
- “Reload drops connections.” — opposite. SIGHUP keeps existing connections on old workers and routes new ones to new workers; old workers exit when drained.
- “Workers share state.” — they don’t (apart from a small shared-memory zone for things like
limit_reqcounters and the cache index). Each worker is independent.
Interview angle
- “Why is nginx faster than Apache for serving thousands of concurrent connections?” — event-driven non-blocking workers vs Apache’s process/thread per request. Nginx handles 10k connections per worker in a few KB each; Apache prefork uses MB per connection.
- “How many worker processes should you configure?” — one per CPU core (
worker_processes auto). More doesn’t help; CPUs are the bottleneck. - “What happens during
nginx -s reload?” — master re-reads config, spawns new workers, sends SIGQUIT to old workers; they finish in-flight requests then exit. Zero-downtime. - “What do workers share?” — listening sockets (so any worker can accept), and small shared-memory zones (cache index, rate-limit counters). No request state.
- “Why is the worker single-threaded?” — no locking, no thread-safety overhead, simpler code, no context switching within a worker. Use multiple workers for multi-core.
- “What’s
sendfileand why does it matter?” — kernel-level “copy this file to this socket” without the bytes ever entering user-space memory. Why nginx serves static files at near-line-rate.