1. How Nginx Works — Event-Driven Architecture

Apache assigns one thread (or process) per connection. Under 10,000 concurrent users that is 10,000 threads — each consuming stack memory, CPU time-slices, and OS scheduler overhead. Nginx takes a different approach: it runs a small, fixed pool of worker processes (one per CPU core is the typical setting). Each worker uses a non-blocking event loop to manage thousands of connections simultaneously, spending no CPU time on idle connections.

This makes Nginx dramatically more efficient under high concurrency. A single Nginx worker can handle 10,000+ simultaneous connections. For static file serving and reverse proxying, Nginx consistently outperforms Apache 5–10x on throughput benchmarks at high concurrency. Understanding this model explains why the tuning parameters below exist.

2. Config File Structure

Nginx configuration is a hierarchy of contexts. Directives are inherited from parent contexts and can be overridden in child contexts.

nginx.conf — context hierarchy
# Main context — global settings
user  www-data;
worker_processes  auto;
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    # Connection handling
    worker_connections  1024;
    use                 epoll;
    multi_accept        on;
}

http {
    # Shared settings for all virtual hosts
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    server {
        # Virtual host (site) configuration
        listen  80;
        server_name  example.com;

        location / {
            # Request routing rules
        }
    }
}

In most Linux distributions, the main nginx.conf includes /etc/nginx/conf.d/*.conf or /etc/nginx/sites-enabled/*. Keep your site-specific configuration in a separate file — one server block per file — and never edit the main nginx.conf directly beyond global settings.

Essential Commands

nginx -t — test configuration for syntax errors before reloading
nginx -s reload — reload config without dropping connections (graceful)
nginx -s stop — fast shutdown
systemctl reload nginx — same as reload via systemd

3. Worker & Connection Tuning

These settings live at the top of nginx.conf and in the events block. They have the largest impact on throughput and latency under high load.

/etc/nginx/nginx.conf — worker & connection tuning
worker_processes  auto;          # one per CPU core
worker_rlimit_nofile  65535;     # max open files per worker (match ulimit -n)

events {
    worker_connections  4096;    # max simultaneous connections per worker
    use  epoll;                  # Linux kernel I/O event model (fastest on Linux)
    multi_accept  on;            # accept all pending connections at once
}

http {
    sendfile        on;          # use kernel sendfile() — zero-copy for static files
    tcp_nopush      on;          # send headers in one packet with sendfile
    tcp_nodelay     on;          # disable Nagle for low-latency keepalive responses

    keepalive_timeout  65;       # keep connection open for 65s after last request
    keepalive_requests 1000;     # max requests per keepalive connection

    types_hash_max_size  2048;
    server_tokens  off;          # hide Nginx version in headers (security)

    # Optimise buffer sizes for typical responses
    client_body_buffer_size      128k;
    client_max_body_size         20m;   # max upload size
    client_header_buffer_size    1k;
    large_client_header_buffers  4 8k;

    open_file_cache              max=10000 inactive=30s;
    open_file_cache_valid        60s;
    open_file_cache_min_uses     2;
    open_file_cache_errors       on;   # cache 404s too — prevents repeated disk lookups
}
DirectiveWhy It MattersRecommended Value
worker_processesOne worker per CPU core saturates the hardware without context-switch overheadauto
worker_connectionsTotal max connections = worker_processes × worker_connections1024–8192 depending on RAM
sendfileBypasses userspace buffer copy for static files — cuts CPU ~30% for file-heavy siteson
keepalive_timeoutAmortises TCP handshake over multiple requests; too high holds connections open unnecessarily15–65 seconds
open_file_cacheCaches file descriptors, reducing syscalls for repeated static file requestsmax=10000 inactive=30s

4. Gzip & Brotli Compression

Compression reduces transferred bytes by 60–80% for text content. The tradeoff is CPU — compression levels 1–6 are fast enough that the bandwidth saving nearly always wins.

http block — gzip configuration
gzip               on;
gzip_vary          on;          # Vary: Accept-Encoding header for CDNs
gzip_proxied       any;         # compress proxied responses too
gzip_comp_level    6;           # 1 (fast) – 9 (best); 6 is the sweet spot
gzip_min_length    1000;        # skip files smaller than 1KB — not worth it
gzip_buffers       16 8k;
gzip_http_version  1.1;
gzip_disable       "msie6";     # disable for ancient IE6
gzip_types
    text/plain
    text/css
    text/xml
    text/javascript
    application/javascript
    application/x-javascript
    application/json
    application/xml
    application/xml+rss
    application/atom+xml
    image/svg+xml
    font/ttf
    font/opentype
    application/vnd.ms-fontobject;
# Do NOT add image/jpeg, image/png, image/webp — already compressed

Brotli — Better Compression for Modern Browsers

Brotli (developed by Google) achieves 15–25% better compression than gzip at equivalent speeds for most text content. It requires the ngx_brotli module — not included in the default Nginx build but available in many package managers.

http block — Brotli (requires ngx_brotli module)
brotli              on;
brotli_comp_level   6;
brotli_static       on;         # serve pre-compressed .br files if they exist
brotli_types
    text/plain text/css text/javascript
    application/javascript application/json
    application/xml image/svg+xml;

Gzip vs Brotli

Serve Brotli where supported (all modern browsers) and fall back to gzip. Nginx with ngx_brotli handles this automatically — it checks the Accept-Encoding header and serves the best available format.

5. Static File Serving & Browser Caching

Browser caching eliminates repeat downloads. The strategy is: set very long max-age for fingerprinted assets (cache-busted by filename hash), and short or no-cache for HTML files that must reflect deployments immediately.

server block — static files with browser cache headers
server {
    listen 443 ssl http2;
    server_name example.com;
    root /var/www/html;

    # HTML — always revalidate (fast check, short cache)
    location ~* \.html?$ {
        expires -1;
        add_header Cache-Control "no-cache, no-store, must-revalidate";
    }

    # Hashed/fingerprinted assets — cache forever
    location ~* \.(js|css)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Images, fonts — long cache
    location ~* \.(jpg|jpeg|png|gif|webp|ico|svg|woff|woff2|ttf|eot)$ {
        expires 6M;
        add_header Cache-Control "public";
        add_header Vary "Accept-Encoding";
        access_log off;
    }

    # Disable access log for favicon and robots
    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    # Deny access to hidden files (.git, .env, etc.)
    location ~ /\. { deny all; }
}

The Immutable Directive

immutable tells the browser: "this file will never change during its cache lifetime — don't even send a conditional request." Only use it on assets where the filename changes on every deploy (hashed filenames like app.a3f9c1d.js). Never use it on filenames that stay the same across deployments.

6. Reverse Proxy Setup

A reverse proxy forwards client requests to a backend application — Node.js, PHP-FPM, Python (Gunicorn/uWSGI), Java (Tomcat), or any process listening on a local port or Unix socket. Nginx handles the public-facing connection, SSL, compression, and rate limiting while the backend focuses on business logic.

server block — reverse proxy to Node.js on port 3000
upstream nodejs_backend {
    server 127.0.0.1:3000;
    keepalive 32;               # keep 32 connections open to backend
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    location / {
        proxy_pass         http://nodejs_backend;

        # Pass real client info to the backend
        proxy_set_header   Host              $host;
        proxy_set_header   X-Real-IP         $remote_addr;
        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;

        # HTTP/1.1 required for keepalive to upstream
        proxy_http_version 1.1;
        proxy_set_header   Connection "";

        # Timeouts
        proxy_connect_timeout  10s;
        proxy_send_timeout     60s;
        proxy_read_timeout     60s;

        # Buffering — disable for streaming/SSE, keep on for normal requests
        proxy_buffering         on;
        proxy_buffer_size       16k;
        proxy_buffers           4 32k;
        proxy_busy_buffers_size 64k;
    }
}

PHP-FPM via Unix Socket

Unix sockets are faster than TCP loopback for same-host PHP-FPM communication — no TCP overhead, no port binding.

server block — PHP-FPM via Unix socket
server {
    listen 443 ssl http2;
    server_name www.example.com;
    root  /var/www/html;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass   unix:/run/php/php8.2-fpm.sock;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include        fastcgi_params;

        # Performance
        fastcgi_buffers          16 16k;
        fastcgi_buffer_size      32k;
        fastcgi_connect_timeout  10s;
        fastcgi_send_timeout     60s;
        fastcgi_read_timeout     60s;
    }
}

try_files — Critical for PHP Apps

try_files $uri $uri/ /index.php?$query_string first tries to serve a real file, then a real directory, and only falls back to PHP if neither exists. This avoids passing static file requests to PHP-FPM and is required by most PHP frameworks (Laravel, WordPress, CodeIgniter).

7. Proxy Caching — FastCGI Cache & proxy_cache

Nginx can cache backend responses on disk and in memory — serving cached pages at static-file speed with zero backend load. For high-traffic PHP sites, FastCGI caching can reduce backend hits by 90%+.

FastCGI Cache for PHP

nginx.conf http block + server block — FastCGI cache
# ── Define cache zone in http block ──────────────────────────
fastcgi_cache_path /var/cache/nginx/fcgi
    levels=1:2
    keys_zone=php_cache:100m      # 100MB shared memory for keys
    max_size=1g                   # 1GB disk cache
    inactive=60m                  # purge entries not accessed in 60 min
    use_temp_path=off;

fastcgi_cache_key "$scheme$request_method$host$request_uri";

# ── In server block ───────────────────────────────────────────
server {
    set $skip_cache 0;

    # Skip cache for logged-in users, POST requests, cart/checkout
    if ($request_method = POST)           { set $skip_cache 1; }
    if ($query_string != "")              { set $skip_cache 1; }
    if ($request_uri ~* "/wp-admin|/wp-login|/cart|/checkout|/account") {
        set $skip_cache 1;
    }
    if ($http_cookie ~* "comment_author|wordpress_logged_in|woocommerce_items_in_cart") {
        set $skip_cache 1;
    }

    location ~ \.php$ {
        fastcgi_pass   unix:/run/php/php8.2-fpm.sock;
        include        fastcgi_params;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;

        fastcgi_cache        php_cache;
        fastcgi_cache_valid  200 301 302  60m;  # cache 200/301/302 for 60 min
        fastcgi_cache_use_stale error timeout updating
                             http_500 http_503;  # serve stale on backend failure
        fastcgi_cache_bypass $skip_cache;
        fastcgi_no_cache     $skip_cache;
        fastcgi_cache_lock   on;                # one request rebuilds cache, others wait

        add_header X-Cache-Status $upstream_cache_status;  # debug: HIT/MISS/BYPASS
    }
}

Proxy Cache for Node.js / Backend APIs

proxy_cache for upstream backends
# http block
proxy_cache_path /var/cache/nginx/proxy
    levels=1:2
    keys_zone=api_cache:50m
    max_size=500m
    inactive=30m
    use_temp_path=off;

proxy_cache_key "$scheme$host$request_uri$http_accept_encoding";

# server/location block
location /api/public/ {
    proxy_pass         http://nodejs_backend;
    proxy_cache        api_cache;
    proxy_cache_valid  200  10m;          # cache successful responses 10 min
    proxy_cache_valid  404  1m;
    proxy_cache_use_stale error timeout updating http_502 http_503 http_504;
    proxy_cache_background_update on;     # update in background, serve stale immediately
    proxy_cache_lock   on;

    # Only cache GET and HEAD — never cache POST/PUT/DELETE
    proxy_cache_methods GET HEAD;

    add_header X-Cache-Status $upstream_cache_status;
}

X-Cache-Status Header

Add add_header X-Cache-Status $upstream_cache_status during development. Check the response in browser DevTools: HIT = served from cache, MISS = fetched from backend, BYPASS = cache skipped, EXPIRED = stale entry being refreshed. Remove or restrict this header in production to avoid leaking cache topology.

8. SSL/TLS Configuration & HTTP/2

A secure, performant TLS setup requires selecting modern protocols and cipher suites, enabling session resumption, and configuring OCSP stapling. Using Let's Encrypt with Certbot handles certificate issuance automatically.

server block — production SSL/TLS + HTTP/2
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com www.example.com;

    # Certificate files (Let's Encrypt path)
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;

    # Modern TLS only — no TLS 1.0 / 1.1
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;

    # Session resumption — reduces handshake overhead for returning clients
    ssl_session_timeout 1d;
    ssl_session_cache   shared:MozSSL:10m;  # ~40,000 sessions
    ssl_session_tickets off;                 # disabled for perfect forward secrecy

    # OCSP stapling — attach certificate status to TLS handshake
    ssl_stapling        on;
    ssl_stapling_verify on;
    resolver            1.1.1.1 8.8.8.8 valid=300s;
    resolver_timeout    5s;

    # HSTS — force HTTPS for 1 year, include subdomains
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    # Diffie-Hellman params (generate with: openssl dhparam -out /etc/nginx/dhparam.pem 2048)
    ssl_dhparam /etc/nginx/dhparam.pem;
}

# HTTP → HTTPS redirect
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

HTTP/2 vs HTTP/3 (QUIC)

HTTP/2 (listen 443 ssl http2) multiplexes multiple requests over one TCP connection, eliminating head-of-line blocking at the HTTP layer. HTTP/3 uses QUIC (UDP-based) to eliminate TCP head-of-line blocking entirely — supported in Nginx 1.25+ with listen 443 quic reuseport. For most production sites, HTTP/2 is the right choice today.

9. Load Balancing

Nginx's upstream module distributes requests across multiple backend servers. This is used for horizontal scaling (multiple app server instances) and for blue-green deployments.

upstream block — load balancing strategies
# ── Round Robin (default) — each request to next server ──────
upstream app_servers {
    server 10.0.0.1:3000;
    server 10.0.0.2:3000;
    server 10.0.0.3:3000;
    keepalive 32;
}

# ── Least Connections — new request to server with fewest active ─
upstream app_least_conn {
    least_conn;
    server 10.0.0.1:3000;
    server 10.0.0.2:3000;
}

# ── IP Hash — sticky sessions (same client → same server) ────
upstream app_sticky {
    ip_hash;
    server 10.0.0.1:3000;
    server 10.0.0.2:3000;
}

# ── Weighted — send more traffic to faster servers ────────────
upstream app_weighted {
    server 10.0.0.1:3000  weight=3;   # gets 3× more requests
    server 10.0.0.2:3000  weight=1;
}

# ── Health checks & failover ──────────────────────────────────
upstream app_with_failover {
    server 10.0.0.1:3000  max_fails=3  fail_timeout=30s;
    server 10.0.0.2:3000  max_fails=3  fail_timeout=30s;
    server 10.0.0.3:3000  backup;     # only used when primary servers are down
}
StrategyBest ForLimitation
Round RobinStateless apps, equal-capacity serversIgnores actual server load
Least ConnectionsLong-lived requests (WebSockets, file uploads)Requires Nginx Plus for active health checks
IP HashSession-based apps without Redis session storagePoor distribution if clients share a NAT IP
WeightedMixed-capacity fleet (some servers have more CPU/RAM)Static weights — doesn't adapt to load

10. Rate Limiting & DDoS Mitigation

Rate limiting restricts how many requests a client can make in a time window. It protects login endpoints from brute-force attacks, limits API abuse, and reduces impact from bot traffic.

http + server block — rate limiting
# ── Define zones in http block ───────────────────────────────
# $binary_remote_addr uses 7 bytes vs 15+ for $remote_addr — more efficient
limit_req_zone  $binary_remote_addr  zone=general:10m   rate=20r/s;
limit_req_zone  $binary_remote_addr  zone=login:10m     rate=5r/m;
limit_req_zone  $binary_remote_addr  zone=api:10m       rate=100r/m;

limit_conn_zone $binary_remote_addr  zone=perip:10m;
limit_conn_zone $server_name         zone=perserver:10m;

# ── Apply in server/location blocks ──────────────────────────
server {
    # Connection limit — max 20 concurrent connections per IP
    limit_conn  perip       20;
    limit_conn  perserver   1000;

    # General pages — burst of 40 allowed, delayed after 20r/s
    location / {
        limit_req  zone=general  burst=40  nodelay;
    }

    # Login endpoint — strict: 5 requests/min, burst of 10
    location /login {
        limit_req  zone=login  burst=10  nodelay;
        limit_req_status 429;               # return 429 Too Many Requests
    }

    # API endpoints — 100 req/min with small burst
    location /api/ {
        limit_req  zone=api  burst=20  nodelay;
        limit_req_status 429;
        add_header Retry-After 60;
    }
}

nodelay vs delay

nodelay processes burst requests immediately without queuing — better for API responses where latency matters. Without nodelay, burst requests are queued and delayed to match the rate — better for protecting backends from sudden spikes without dropping requests.

11. Security Hardening

A few targeted Nginx directives significantly reduce the attack surface of your web server.

server block — security headers & hardening
# ── Hide server information ───────────────────────────────────
server_tokens  off;               # don't expose Nginx version

# ── Security headers ──────────────────────────────────────────
add_header X-Frame-Options           "SAMEORIGIN"                     always;
add_header X-Content-Type-Options    "nosniff"                        always;
add_header X-XSS-Protection          "1; mode=block"                  always;
add_header Referrer-Policy           "strict-origin-when-cross-origin" always;
add_header Permissions-Policy        "geolocation=(), microphone=(), camera=()" always;
add_header Content-Security-Policy
    "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self'"
    always;

# ── Block common attack patterns ──────────────────────────────
# Block requests with suspicious query strings
if ($query_string ~* "union.*select|select.*from|insert.*into|delete.*from|drop.*table|exec\(|cast\(|convert\(|0x[0-9a-f]") {
    return 403;
}

# Deny access to sensitive files
location ~ /\.(git|env|htaccess|htpasswd|DS_Store) {
    deny all;
    return 404;
}
location ~* \.(bak|sql|dump|tar|gz|zip|log)$ {
    deny all;
    return 404;
}

# Block bad bots and scanners
if ($http_user_agent ~* "nikto|sqlmap|nmap|masscan|zgrab|python-requests|curl/7.") {
    return 403;
}

# Limit HTTP methods — only allow what your app uses
if ($request_method !~ ^(GET|POST|HEAD|OPTIONS|PUT|DELETE)$) {
    return 405;
}

12. Complete Production Config Example

The following combines all the above into a single production-ready Nginx configuration for a PHP application with FastCGI caching, SSL/TLS, gzip, browser caching, rate limiting, and security headers.

/etc/nginx/sites-available/example.com — complete config
# FastCGI cache zone — defined before server blocks
fastcgi_cache_path /var/cache/nginx/fcgi
    levels=1:2  keys_zone=FCGI:100m  max_size=2g  inactive=60m  use_temp_path=off;

# Rate limit zones
limit_req_zone  $binary_remote_addr  zone=general:10m  rate=30r/s;
limit_req_zone  $binary_remote_addr  zone=login:10m    rate=5r/m;
limit_conn_zone $binary_remote_addr  zone=perip:10m;

# ── HTTP → HTTPS redirect ─────────────────────────────────────
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

# ── Main HTTPS server ─────────────────────────────────────────
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com www.example.com;
    root  /var/www/html;
    index index.php;

    # TLS
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_stapling        on;
    ssl_stapling_verify on;
    resolver            1.1.1.1 valid=300s;

    # Security headers
    server_tokens off;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Frame-Options        "SAMEORIGIN"  always;
    add_header X-Content-Type-Options "nosniff"     always;
    add_header Referrer-Policy        "strict-origin-when-cross-origin" always;

    # Rate limiting
    limit_conn perip 30;
    limit_req  zone=general  burst=60  nodelay;

    # Gzip
    gzip on;  gzip_vary on;  gzip_comp_level 6;  gzip_min_length 1000;
    gzip_types text/plain text/css application/javascript application/json image/svg+xml;

    # Static files — long cache
    location ~* \.(css|js|woff2?)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        access_log off;
    }
    location ~* \.(jpg|jpeg|png|gif|webp|ico|svg)$ {
        expires 6M;
        add_header Cache-Control "public";
        access_log off;
    }

    # Deny hidden files
    location ~ /\. { deny all; return 404; }

    # Routing
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # Strict rate limit for login
    location ~ ^/(login|wp-login) {
        limit_req zone=login burst=10 nodelay;
        try_files $uri /index.php?$query_string;
        fastcgi_pass  unix:/run/php/php8.2-fpm.sock;
        include       fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    # PHP — with FastCGI caching
    set $skip_cache 0;
    if ($request_method = POST)     { set $skip_cache 1; }
    if ($query_string != "")        { set $skip_cache 1; }
    if ($request_uri ~* "/admin|/login|/cart|/checkout|/account") {
        set $skip_cache 1;
    }
    if ($http_cookie ~* "logged_in|session|cart") {
        set $skip_cache 1;
    }

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass   unix:/run/php/php8.2-fpm.sock;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include        fastcgi_params;
        fastcgi_buffers 16 16k;  fastcgi_buffer_size 32k;

        fastcgi_cache        FCGI;
        fastcgi_cache_valid  200 60m;
        fastcgi_cache_use_stale error timeout updating http_500;
        fastcgi_cache_bypass $skip_cache;
        fastcgi_no_cache     $skip_cache;
        fastcgi_cache_lock   on;
        add_header X-Cache-Status $upstream_cache_status;
    }
}

Testing & Monitoring Your Config

Test syntax: nginx -t before every reload.
Reload gracefully: systemctl reload nginx
Check cache hits: grep "X-Cache-Status: MISS" /var/log/nginx/access.log | wc -l
Check error rate: tail -f /var/log/nginx/error.log
Test SSL grade: SSL Labs — aim for A+.
Test performance: ab -n 1000 -c 50 https://example.com/ (Apache Bench)