Backend / Protocols / nginx / 02_architecture.md

Nginx Architecture

Updated 6 interview angles 5 min read source
On this page10
  1. Master / worker model
  2. Event loop, not threads
  3. Why workers are single-threaded
  4. Why nginx scales
  5. What nginx is good at
  6. What nginx is not good at
  7. Reload vs restart
  8. Connection lifecycle, simplified
  9. Common interview confusions
  10. Interview angle

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

text
   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 (HUP for reload, USR2 for 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:

nginx
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:

text
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 filessendfile() 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

bash
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

text
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_req counters and the cache index). Each worker is independent.

Interview angle 6

  • “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 sendfile and 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.