Page 1 of 2
TLS termination means Nginx is the machine that decrypts incoming HTTPS traffic, handling the TLS handshake and certificate work itself, before the request ever reaches your application. Doing this at Nginx rather than in each backend app has real advantages: certificate renewal and configuration live in one place instead of being duplicated across every service, application code never has to touch TLS at all, and Nginx's TLS handling is fast, well-optimized, and can offload connections that would otherwise burn CPU cycles better spent on business logic.
server {
listen 443 ssl;
server_name app.example.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem; # your cert + intermediate CA certs, concatenated
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://127.0.0.1:3000; # plain HTTP from here to the backend
}
}
ssl_certificate needs to point at a full chain: your own certificate followed by whatever intermediate certificates link it back to a trusted root, all concatenated into one file. Miss the intermediates and browsers that don't already trust your CA directly will fail the handshake, even though the certificate itself is technically valid. In the example above, once TLS is terminated at Nginx, the connection onward to the backend is plain HTTP, which is fine when Nginx and the backend share a trusted private network. If that internal hop crosses something less trusted (a different VPC, a compliance boundary that requires encryption everywhere, like PCI-DSS scope), you instead re-encrypt to the backend by using proxy_pass https:// with its own certificate configuration, meaning Nginx is decrypting the client's connection and then opening a fresh, separate TLS connection of its own to the backend. Terminating and re-encrypting are genuinely different operations doing different jobs, and conflating them is a common source of confusion when someone asks "is traffic to my backend encrypted?" and the honest answer depends entirely on which of these two you actually configured.
limit_req_zone (in the http block) defines a shared memory zone tracking request rates by some key, usually the client IP, and limit_req applies it to a location.
http {
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
server {
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
}
}
}
Picture a flash-sale endpoint, or just one misbehaving script hammering your API. rate=10r/s caps sustained traffic per client IP at 10 requests per second. burst=20 allows a short spike above that rate, up to 20 extra requests queued, before Nginx starts rejecting anything further with 503 Service Unavailable. Without nodelay, those burst requests get held and drip-fed out at the steady rate instead of processed immediately, which adds latency; nodelay lets the burst through right away and only starts rejecting once the burst allowance is used up. This is the difference between a real production incident where one abusive client or a sudden traffic spike takes down a shared backend, versus one where Nginx quietly absorbs it at the edge and everyone else's traffic keeps flowing normally. limit_conn_zone and limit_conn do the same thing for concurrent connection counts rather than request rate, useful for capping how many simultaneous connections one client can hold open.
Two directives set Nginx's real concurrency ceiling, and they connect directly back to operating-system limits.
worker_processes auto; # one worker per CPU core, usually the right default
events {
worker_connections 1024; # max simultaneous connections per worker
}
worker_processes auto spins up one worker per available CPU core, which is almost always what you want since Nginx workers are single-threaded and benefit from one per core to use all the hardware. worker_connections caps how many simultaneous connections each of those workers can hold. Multiply the two together and you get a theoretical max client count, but there's a catch worth internalizing: every connection consumes at least one file descriptor, and a proxied connection actually consumes two, one on the client side, one on the connection to the upstream backend. So the practical ceiling for proxied traffic is closer to half the naive worker_processes × worker_connections number. And that ceiling is itself bounded by the operating system's own file descriptor limit, the same ulimit -n you'd tune for any high-connection-count service. If the OS limit is lower than what your Nginx config assumes, you'll hit "too many open files" errors in the error log under load regardless of what worker_connections says. Nginx has its own worker_rlimit_nofile directive to explicitly raise the per-worker file descriptor limit above the OS default, but raising it in Nginx without also raising the underlying OS limit accomplishes nothing, the two need to move together.
Real scenario-based DevOps questions, hands-on practice, and clear explanations for every answer.