Web ServerWeb Servers
Thuộc bộ kiến thức Backend Roadmap.
Tổng quan
Một web server là chương trình lắng nghe các request HTTP(S) qua network và trả về response HTTP. Theo nghĩa cổ điển, nó phục vụ static content (HTML, CSS, JavaScript, hình ảnh, font) trực tiếp từ disk. Trong kiến trúc hiện đại, nó gần như luôn đảm nhận thêm một vai trò quan trọng hơn: đứng phía trước application của bạn như một reverse proxy — làm TLS termination, routing, load balancing, caching, compression, và che chắn app khỏi lưu lượng internet thô.
Hiểu về web server là quan trọng vì nó là chặng đầu tiên của mọi request đến backend. Cấu hình đúng tầng web server mang lại lợi ích “miễn phí” về performance (caching, compression, tái sử dụng connection), security (TLS, rate limiting, lọc request), và khả năng vận hành (graceful reload, access log, health check) — trước khi một dòng code application nào chạy.
Note này bổ trợ cho hai phần liên quan trong bộ kiến thức:
../devops/vi/06-web-servers-and-proxies.md— góc nhìn vận hành/DevOps (deployment, quản lý config, tự động hóa TLS ở quy mô lớn).- Các topic
network-engineer— góc nhìn networking tầng thấp (TCP, TLS handshake, DNS, HTTP/2, HTTP/3).
Ở đây ta tập trung vào góc nhìn của backend developer: web server nằm ở đâu phía trước app, một reverse proxy thực sự làm gì, và cấu hình nó thế nào cho đúng.
Note liên quan: Internet and Web Fundamentals · Caching · Scalability and Reliability.
Kiến thức nền tảng
Static content vs dynamic content
| Khía cạnh | Static content | Dynamic content |
|---|---|---|
| Định nghĩa | File không đổi theo từng request (HTML, CSS, JS, hình ảnh) | Response tạo ra theo từng request (HTML render từ DB, JSON từ API) |
| Tạo bởi | Web server đọc trực tiếp từ disk | Code application (application server) tính toán |
| Ví dụ | /logo.png, /app.css, /index.html | /api/orders/42, /dashboard sau khi đăng nhập |
| Khả năng cache | Rất cao — thường được cache mạnh ở CDN và browser | Tùy — phụ thuộc mức độ cá nhân hóa và độ tươi (freshness) |
| Ai phục vụ tốt nhất | Web server (Nginx/Apache) hoặc CDN | Application server phía sau web server |
Nguyên tắc tốt: để web server phục vụ static asset (nó được tối ưu cho sendfile, byte range, caching header) và để application server xử lý logic động. Web server sẽ proxy các request động tới app.
Đường đi của request phía trước application server
Luồng request điển hình của một web app hiện đại:
- 01Client (browser / mobile)
- HTTPS02CDNtùy chọn — edge cache cho static asset
- 03Load balancer (L4/L7)
- 04Web server / reverse proxy (Nginx)
- phục vụ static, proxy request động05Application serverGunicorn / Node / Puma / Kestrel
- 06Database, cache, queue
Web server / reverse proxy là ranh giới giữa “public edge” và “phần ruột application”. Nó nói HTTP với thế giới bên ngoài và forward tới app qua một upstream connection riêng tư, thường là plaintext (HTTP hoặc Unix socket).
Vì sao không expose application server trực tiếp?
Đa số application server có thể nói HTTP trực tiếp, nhưng nhìn chung bạn không nên expose chúng ra internet:
- App server được tối ưu để chạy code của bạn, chứ không phải để xử lý slow client, TLS ở quy mô lớn, hay phục vụ file hiệu quả.
- Reverse proxy buffer các slow client để một người upload chậm không chiếm giữ một app worker đắt đỏ (giảm thiểu vấn đề kiểu slow-loris).
- Proxy tập trung hóa các mối quan tâm xuyên suốt (TLS, gzip, rate limiting, header, logging) để mỗi app không phải tự cài lại từ đầu.
Khái niệm chính
Web server vs application server vs reverse proxy vs API gateway vs load balancer
Các thuật ngữ này chồng lấn nhau, và một sản phẩm (như Nginx) có thể đóng nhiều vai trò cùng lúc. Sự phân biệt là về trách nhiệm, không nhất thiết là các máy riêng biệt.
| Thành phần | Nhiệm vụ chính | Hoạt động ở tầng | Ví dụ điển hình |
|---|---|---|---|
| Web server | Nhận request HTTP; phục vụ static file; thường kiêm reverse proxy | L7 (HTTP) | Nginx, Apache httpd, Caddy, IIS |
| Application server | Chạy code application; tạo ra response động | L7 (HTTP/app protocol) | Gunicorn/uWSGI (Python), Puma (Ruby), Kestrel (.NET), Node.js, Tomcat (Java) |
| Reverse proxy | Đứng trước một hoặc nhiều server; forward request thay cho chúng | L7 | Nginx, HAProxy, Envoy, Traefik, Caddy |
| API gateway | Reverse proxy chuyên cho API: auth, rate limiting, quota, routing, transform request/response, versioning | L7 | Kong, Amazon API Gateway, Apigee, gateway dựa trên Envoy |
| Load balancer | Phân phối lưu lượng qua nhiều backend để scale và tăng tính sẵn sàng | L4 (TCP/UDP) hoặc L7 (HTTP) | HAProxy, Nginx, AWS ELB/ALB/NLB, Envoy |
Một vài điểm cần làm rõ:
- Reverse proxy forward request tới các backend thay mặt cho client. Forward proxy thì ngược lại — forward request từ client ra internet (ví dụ egress proxy của công ty). Chữ “reverse” là xét từ góc nhìn của server.
- Load balancer là một reverse proxy mà trọng tâm chính là phân phối + health check. Load balancer L4 định tuyến theo IP/port mà không đọc HTTP; load balancer L7 đọc HTTP và có thể định tuyến theo path/host/header.
- API gateway là reverse proxy cộng thêm policy đặc thù cho API (authentication, API key, rate limit, chỉnh sửa request). Nó là edge “thông minh” cho lưu lượng API.
- Các vai trò này kết hợp được: internet → load balancer → nhiều Nginx reverse proxy → app server là một layout phổ biến và lành mạnh.
Trách nhiệm của reverse proxy
Reverse proxy chứng minh giá trị của mình bằng cách xử lý các mối quan tâm xuyên suốt ở một nơi:
| Trách nhiệm | Làm gì | Vì sao quan trọng |
|---|---|---|
| TLS termination | Giải mã HTTPS ở edge; nói plaintext/HTTP với backend | Tập trung quản lý certificate; giảm tải crypto khỏi app |
| Routing | Chọn backend dựa trên host, path, method, hoặc header | Một entry point phục vụ được nhiều app/microservice |
| Load balancing | Trải request qua các upstream instance; bỏ qua instance không khỏe | Scale ngang và tính sẵn sàng |
| Caching | Lưu response và phục vụ mà không cần chạm backend | Giảm mạnh latency và tải cho content cache được |
| Compression | Nén gzip/brotli response ngay lúc trả về | Payload nhỏ hơn, tải trang nhanh hơn |
| Rate limiting | Giới hạn tốc độ request theo client/key | Bảo vệ backend khỏi lạm dụng và đỉnh lưu lượng |
| Buffering | Đọc trọn vẹn request/response của slow client trước/sau khi chạm app | Giải phóng app worker khỏi slow client |
| Chỉnh sửa header | Thêm/bớt header (X-Forwarded-For, HSTS, security header) | Thông tin client đúng, tăng cường bảo mật |
| Phục vụ static | Phục vụ file trực tiếp, không qua app | Giao asset nhanh |
TLS termination
Proxy giữ certificate và private key, terminate TLS, rồi forward plaintext HTTP tới upstream (qua network riêng đáng tin) — hoặc mã hóa lại (TLS passthrough / re-encryption) khi chặng tới backend cũng cần được mã hóa. Terminate ở edge nghĩa là bạn gia hạn và quản lý certificate ở một chỗ. Caddy còn tiến xa hơn với automatic HTTPS qua ACME/Let’s Encrypt ngay từ mặc định.
Thuật toán load balancing
| Thuật toán | Hành vi | Phù hợp cho |
|---|---|---|
| Round robin | Request phân phối theo thứ tự | Backend đồng nhất, stateless (mặc định) |
| Least connections | Gửi tới backend có ít active connection nhất | Request không đều hoặc kéo dài |
| IP hash | Cùng client IP → cùng backend | Sticky session mà không cần shared session store |
| Weighted | Weight lớn hơn → nhiều lưu lượng hơn | Backend có năng lực khác nhau |
Health check (passive: đánh dấu backend down sau N lần lỗi; active: probe một endpoint) giữ lưu lượng tránh xa các instance đã chết.
Mô hình process và concurrency
Cách một web server xử lý nhiều connection đồng thời quyết định profile performance của nó.
| Mô hình | Cách hoạt động | Dùng bởi |
|---|---|---|
| Event-driven / async | Vài worker process, mỗi cái là một event loop xử lý hàng nghìn connection qua epoll/kqueue | Nginx, Caddy, Envoy |
| Process/thread mỗi connection | Mỗi connection do một process hoặc thread xử lý | Apache MPM prefork/worker (cổ điển) |
| Hybrid | Worker process, mỗi cái có một pool thread/event | Apache MPM event |
Server event-driven (Nginx, Caddy) scale được lên mức concurrency rất cao với bộ nhớ thấp vì không cấp phát một thread/process cho mỗi connection. Đây là lý do Nginx trở thành reverse proxy chuẩn cho các site lưu lượng cao.
Keep-alive, timeout và buffering
- Keep-alive tái sử dụng một connection TCP (và TLS) cho nhiều request, tránh handshake lặp lại. Cấu hình
keepalive_timeoutcho client và một poolkeepalivetới upstream. - Timeout giới hạn thời gian server chờ ở mỗi giai đoạn:
client_header_timeout,client_body_timeout,send_timeout, và với upstream làproxy_connect_timeout/proxy_read_timeout. Timeout hợp lý ngăn cạn kiệt tài nguyên. - Buffering (
proxy_buffering on) cho phép proxy đọc trọn response từ upstream, giải phóng app worker nhanh. Tắt nó (proxy_buffering off) cho response dạng streaming/SSE/long-polling.
Best Practices
Khảo sát các web server
| Server | Mô hình | Kiểu config | Điểm mạnh nổi bật | Điểm cần lưu ý |
|---|---|---|---|---|
| Nginx | Event-driven | Block khai báo trong nginx.conf | Reverse proxy nhanh, load balancing, caching; ecosystem lớn | TLS phải cấu hình thủ công; một số tính năng là bản thương mại (Nginx Plus) |
| Apache httpd | Process/thread (MPM) | Directive, .htaccess | Trưởng thành, module linh hoạt, override .htaccess theo thư mục | Tốn bộ nhớ hơn khi concurrency cao ở chế độ prefork |
| Caddy | Event-driven (Go) | Caddyfile đơn giản / JSON | Automatic HTTPS qua ACME; mặc định hợp lý; HTTP/3 | Ecosystem nhỏ hơn; ít “bí kíp” tuning |
| Microsoft IIS | Native trên Windows | GUI + web.config | Tích hợp sâu Windows/.NET, auth qua Active Directory | Chỉ chạy trên Windows |
| App-embedded server | Tùy (async/threaded) | Config trong code/framework | Không thêm chặng; setup dev đơn giản (Express, Kestrel, Gunicorn, Uvicorn) | Trong production thường đặt reverse proxy phía trước để lo TLS, static, buffering |
Kinh nghiệm chung: Nginx hoặc Caddy làm reverse proxy phía trước; embedded/app server của framework nằm phía sau. Chọn Caddy nếu muốn HTTPS zero-config; chọn Nginx nếu muốn kiểm soát tối đa và ecosystem lớn.
Một config reverse-proxy Nginx thực tế
Reverse proxy tới upstream app, có TLS, gzip và caching response cơ bản:
# /etc/nginx/nginx.conf (http context — chỉ hiển thị các directive chính)
# Định nghĩa proxy cache zone: 10 MB cho key, tối đa 1 GB trên disk.
proxy_cache_path /var/cache/nginx/app
levels=1:2
keys_zone=app_cache:10m
max_size=1g
inactive=60m
use_temp_path=off;
# Rate limit: 10 request/giây mỗi client IP, cho phép burst nhỏ.
limit_req_zone $binary_remote_addr zone=req_per_ip:10m rate=10r/s;
# Pool upstream gồm các application instance.
upstream app_backend {
least_conn; # gửi tới instance ít bận nhất
server 127.0.0.1:8000 max_fails=3 fail_timeout=15s;
server 127.0.0.1:8001 max_fails=3 fail_timeout=15s;
keepalive 32; # tái sử dụng connection tới upstream
}
# Redirect toàn bộ HTTP plaintext sang HTTPS.
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name example.com www.example.com;
# --- TLS termination ---
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 HIGH:!aNULL:!MD5;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
# --- Security header ---
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
# --- Nén gzip ---
gzip on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_types text/plain text/css application/json application/javascript
text/xml application/xml image/svg+xml;
gzip_vary on;
# --- Phục vụ static asset trực tiếp, cache mạnh ở browser ---
location /static/ {
root /var/www/example;
expires 30d;
access_log off;
add_header Cache-Control "public, immutable";
}
# --- Proxy request động tới app, có caching response ---
location / {
limit_req zone=req_per_ip burst=20 nodelay;
proxy_pass http://app_backend;
# Giữ lại thông tin client cho app.
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;
# Dùng HTTP/1.1 + keep-alive tới upstream.
proxy_http_version 1.1;
proxy_set_header Connection "";
# Timeout.
proxy_connect_timeout 5s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
# Caching response cho các response GET/HEAD cache được.
proxy_cache app_cache;
proxy_cache_valid 200 301 302 10m;
proxy_cache_valid 404 1m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on; # gộp các miss đồng thời thành một lần fetch upstream
add_header X-Cache-Status $upstream_cache_status; # HIT / MISS / EXPIRED — rất tiện để debug
}
}
Ghi chú về config:
X-Forwarded-For/X-Forwarded-Protolà thiết yếu để app thấy được IP và scheme thật của client sau khi TLS termination. Hãy đảm bảo framework của bạn được cấu hình chỉ tin tưởng các header này khi đến từ proxy.proxy_cache_lock onngăn hiện tượng “thundering herd” khi nhiều request giống hệt nhau cùng đập vào backend lúc một entry phổ biến hết hạn.X-Cache-Statuscho biết response có được phục vụ từ cache hay không — cực kỳ hữu ích khi tuning. Xem Caching để biết chiến lược cache-key và invalidation.proxy_cache_use_staletăng khả năng chịu lỗi: phục vụ content hơi cũ nếu backend tạm thời down.
Reload mà không rớt connection sau khi sửa config:
nginx -t # validate config trước — đừng bao giờ reload một config lỗi
nginx -s reload # graceful reload: worker mới khởi động, worker cũ drain
Cơ bản về tuning
| Thiết lập | Hướng dẫn |
|---|---|
worker_processes | Đặt auto (một worker mỗi CPU core) cho công việc TLS/compression thiên về CPU |
worker_connections | Tăng lên (ví dụ 1024–65535) đồng thời với giới hạn file-descriptor của OS (ulimit -n) |
keepalive_timeout | 60–75s cho client; giữ một pool keepalive tới upstream để giảm reconnect |
| Timeout | Đặt timeout read/send cho client và proxy để giới hạn dùng tài nguyên |
sendfile / tcp_nopush | Bật để giao static file hiệu quả |
| Logging | Dùng access log có cấu trúc/buffer; tắt access log cho các path static lưu lượng cao |
Best practices vận hành
- Terminate TLS ở edge và giữ certificate ở một nơi; tự động hóa gia hạn (Certbot, hoặc ACME tích hợp sẵn của Caddy).
- Luôn
nginx -ttrước khi reload, và dùng graceful reload để tránh rớt request khi deploy. - Để web server phục vụ static asset; chỉ proxy request động tới app.
- Đặt và forward đúng header (
X-Forwarded-*) và thêm security header (HSTS,nosniff). - Cache có chủ đích: cache những gì an toàn, đặt key đúng, expose
X-Cache-Status, và có kế hoạch invalidation. - Rate-limit và buffer để bảo vệ app worker khỏi lạm dụng và slow client.
- Health-check các upstream và cấu hình
max_fails/fail_timeoutđể lưu lượng tránh instance chết. - Chỉnh timeout sát thực tế: endpoint streaming/SSE cần tắt buffering và tăng read timeout.
- Chạy nhiều instance proxy phía sau một load balancer để chính tầng proxy cũng có tính sẵn sàng — xem Scalability and Reliability.
Tài liệu tham khảo
Part of the Backend Roadmap knowledge base.
Overview
A web server is a program that listens for HTTP(S) requests over the network and returns HTTP responses. In the classic sense it serves static content (HTML, CSS, JavaScript, images, fonts) directly from disk. In modern architectures it almost always plays a second, more important role: sitting in front of your application as a reverse proxy — terminating TLS, routing requests, load balancing, caching, compressing, and shielding the app from raw internet traffic.
Understanding web servers matters because they are the first hop of every request that reaches your backend. Getting the web server layer right gives you free wins in performance (caching, compression, connection reuse), security (TLS, rate limiting, request filtering), and operability (graceful reloads, access logs, health checks) — before a single line of your application code runs.
This note complements two related sections in this knowledge base:
../devops/en/06-web-servers-and-proxies.md— the operations/DevOps angle (deployment, config management, TLS automation at scale).- The
network-engineertopics — the lower-level networking angle (TCP, TLS handshakes, DNS, HTTP/2, HTTP/3).
Here we focus on the backend developer’s view: how a web server fits in front of your app, what a reverse proxy actually does, and how to configure one correctly.
Related notes: Internet and Web Fundamentals · Caching · Scalability and Reliability.
Fundamentals
Static vs dynamic content
| Aspect | Static content | Dynamic content |
|---|---|---|
| Definition | Files that don’t change per request (HTML, CSS, JS, images) | Responses generated per request (HTML rendered from a DB, JSON from an API) |
| Produced by | Read directly from disk by the web server | Computed by application code (app server) |
| Examples | /logo.png, /app.css, /index.html | /api/orders/42, /dashboard after login |
| Cacheability | Very high — often cached aggressively at CDN and browser | Varies — depends on personalization and freshness |
| Who serves it best | Web server (Nginx/Apache) or CDN | Application server behind the web server |
A good rule: let the web server serve static assets (it is optimized for sendfile, byte ranges, caching headers) and let the app server handle dynamic logic. The web server proxies dynamic requests to the app.
The request path in front of an app server
A typical request flow for a modern web app:
- 01Client (browser / mobile)
- HTTPS02CDNoptional — edge cache for static assets
- 03Load balancer (L4/L7)
- 04Web server / reverse proxy (Nginx)
- serves static, proxies dynamic05Application serverGunicorn / Node / Puma / Kestrel
- 06Database, cache, queues
The web server / reverse proxy is the boundary between the “public edge” and your “application internals”. It speaks HTTP to the world and forwards to the app over a private, often plaintext, upstream connection (HTTP or a Unix socket).
Why not expose the app server directly?
Most application servers can speak HTTP directly, but you generally shouldn’t expose them to the internet:
- App servers are optimized for running your code, not for slow-client handling, TLS at scale, or serving files efficiently.
- A reverse proxy buffers slow clients so a single slow uploader doesn’t tie up an expensive app worker (mitigates slow-loris style problems).
- The proxy centralizes cross-cutting concerns (TLS, gzip, rate limiting, headers, logging) so every app doesn’t reimplement them.
Key Concepts
Web server vs application server vs reverse proxy vs API gateway vs load balancer
These terms overlap and a single product (like Nginx) can play several roles at once. The distinction is about responsibility, not necessarily separate machines.
| Component | Primary job | Operates at | Typical examples |
|---|---|---|---|
| Web server | Accept HTTP requests; serve static files; often also reverse-proxy | L7 (HTTP) | Nginx, Apache httpd, Caddy, IIS |
| Application server | Run your application code; produce dynamic responses | L7 (HTTP/app protocol) | Gunicorn/uWSGI (Python), Puma (Ruby), Kestrel (.NET), Node.js, Tomcat (Java) |
| Reverse proxy | Sit in front of one or more servers; forward requests on their behalf | L7 | Nginx, HAProxy, Envoy, Traefik, Caddy |
| API gateway | Reverse proxy specialized for APIs: auth, rate limiting, quotas, routing, request/response transformation, versioning | L7 | Kong, Amazon API Gateway, Apigee, Envoy-based gateways |
| Load balancer | Distribute traffic across many backends for scale and availability | L4 (TCP/UDP) or L7 (HTTP) | HAProxy, Nginx, AWS ELB/ALB/NLB, Envoy |
Key clarifications:
- A reverse proxy forwards requests to backends on behalf of clients. A forward proxy does the opposite — it forwards requests from clients out to the internet (e.g. a corporate egress proxy). “Reverse” is from the server’s perspective.
- A load balancer is a reverse proxy whose main focus is distribution + health checking. An L4 load balancer routes by IP/port without reading HTTP; an L7 load balancer reads HTTP and can route by path/host/header.
- An API gateway is a reverse proxy plus API-specific policy (authentication, API keys, rate limits, request shaping). It is the “smart” edge for API traffic.
- These roles compose: internet → load balancer → several Nginx reverse proxies → app servers is a common, healthy layout.
Reverse proxy responsibilities
A reverse proxy earns its place by handling cross-cutting concerns in one place:
| Responsibility | What it does | Why it matters |
|---|---|---|
| TLS termination | Decrypts HTTPS at the edge; talks plaintext/HTTP to the backend | Centralizes certificate management; offloads crypto from the app |
| Routing | Chooses a backend based on host, path, method, or headers | One entry point can serve many apps/microservices |
| Load balancing | Spreads requests across upstream instances; skips unhealthy ones | Horizontal scale and availability |
| Caching | Stores responses and serves them without hitting the backend | Massive latency and load reduction for cacheable content |
| Compression | gzip/brotli responses on the fly | Smaller payloads, faster page loads |
| Rate limiting | Caps request rate per client/key | Protects backends from abuse and traffic spikes |
| Buffering | Reads full slow-client requests/responses before/after touching the app | Frees app workers from slow clients |
| Header manipulation | Adds/removes headers (X-Forwarded-For, HSTS, security headers) | Correct client info, hardening |
| Static serving | Serves files directly, bypassing the app | Fast asset delivery |
TLS termination
The proxy holds the certificate and private key, terminates TLS, and forwards plaintext HTTP to the upstream (over a trusted private network) — or re-encrypts (TLS passthrough / re-encryption) when the backend hop must also be encrypted. Terminating at the edge means you renew and manage certificates in one place. Caddy takes this further with automatic HTTPS via ACME/Let’s Encrypt out of the box.
Load balancing algorithms
| Algorithm | Behavior | Good for |
|---|---|---|
| Round robin | Requests distributed in order | Homogeneous, stateless backends (default) |
| Least connections | Send to the backend with fewest active connections | Uneven or long-lived requests |
| IP hash | Same client IP → same backend | Sticky sessions without shared session store |
| Weighted | Bigger weight → more traffic | Mixed-capacity backends |
Health checks (passive: mark a backend down after N failures; active: probe an endpoint) keep traffic away from dead instances.
Process and concurrency models
How a web server handles many simultaneous connections shapes its performance profile.
| Model | How it works | Used by |
|---|---|---|
| Event-driven / async | A few worker processes, each an event loop handling thousands of connections via epoll/kqueue | Nginx, Caddy, Envoy |
| Process/thread per connection | Each connection handled by a process or thread | Apache prefork/worker MPMs (classic) |
| Hybrid | Worker processes each with a thread/event pool | Apache event MPM |
Event-driven servers (Nginx, Caddy) scale to very high concurrency with low memory because they don’t allocate a thread/process per connection. This is why Nginx became the standard reverse proxy for high-traffic sites.
Keep-alive, timeouts, and buffering
- Keep-alive reuses one TCP (and TLS) connection for multiple requests, avoiding repeated handshakes. Configure
keepalive_timeoutfor clients and akeepaliveconnection pool to upstreams. - Timeouts bound how long the server waits at each stage:
client_header_timeout,client_body_timeout,send_timeout, and upstreamproxy_connect_timeout/proxy_read_timeout. Sensible timeouts prevent resource exhaustion. - Buffering (
proxy_buffering on) lets the proxy read the whole upstream response, freeing the app worker quickly. Disable it (proxy_buffering off) for streaming/SSE/long-polling responses.
Best Practices
Survey of web servers
| Server | Model | Config style | Standout strengths | Watch-outs |
|---|---|---|---|---|
| Nginx | Event-driven | Declarative nginx.conf blocks | Fast reverse proxy, load balancing, caching; huge ecosystem | Manual TLS setup; some features are commercial (Nginx Plus) |
| Apache httpd | Process/thread (MPMs) | Directives, .htaccess | Mature, flexible modules, per-directory .htaccess overrides | Higher memory under high concurrency in prefork mode |
| Caddy | Event-driven (Go) | Simple Caddyfile / JSON | Automatic HTTPS via ACME; sane defaults; HTTP/3 | Smaller ecosystem; less tuning lore |
| Microsoft IIS | Windows-native | GUI + web.config | Deep Windows/.NET integration, Active Directory auth | Windows-only |
| App-embedded servers | Varies (async/threaded) | Code/framework config | No extra hop; simple dev setup (Express, Kestrel, Gunicorn, Uvicorn) | Usually put a reverse proxy in front for TLS, static, buffering in prod |
Rule of thumb: Nginx or Caddy as the reverse proxy in front; your app framework’s embedded/app server behind it. Caddy if you want zero-config HTTPS; Nginx if you want maximum control and ecosystem.
A real Nginx reverse-proxy config
Reverse proxy to an upstream app, with TLS, gzip, and basic response caching:
# /etc/nginx/nginx.conf (http context — key directives shown)
# Define a proxy cache zone: 10 MB of keys, up to 1 GB on disk.
proxy_cache_path /var/cache/nginx/app
levels=1:2
keys_zone=app_cache:10m
max_size=1g
inactive=60m
use_temp_path=off;
# Rate limit: 10 requests/sec per client IP, with a small burst allowance.
limit_req_zone $binary_remote_addr zone=req_per_ip:10m rate=10r/s;
# Upstream pool of application instances.
upstream app_backend {
least_conn; # send to the least-busy instance
server 127.0.0.1:8000 max_fails=3 fail_timeout=15s;
server 127.0.0.1:8001 max_fails=3 fail_timeout=15s;
keepalive 32; # reuse upstream connections
}
# Redirect all plaintext HTTP to HTTPS.
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name example.com www.example.com;
# --- TLS termination ---
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 HIGH:!aNULL:!MD5;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
# --- Security headers ---
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
# --- gzip compression ---
gzip on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_types text/plain text/css application/json application/javascript
text/xml application/xml image/svg+xml;
gzip_vary on;
# --- Serve static assets directly, cache hard in the browser ---
location /static/ {
root /var/www/example;
expires 30d;
access_log off;
add_header Cache-Control "public, immutable";
}
# --- Proxy dynamic requests to the app, with response caching ---
location / {
limit_req zone=req_per_ip burst=20 nodelay;
proxy_pass http://app_backend;
# Preserve client info for the app.
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;
# Use HTTP/1.1 + keep-alive to the upstream.
proxy_http_version 1.1;
proxy_set_header Connection "";
# Timeouts.
proxy_connect_timeout 5s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
# Response caching for cacheable GET/HEAD responses.
proxy_cache app_cache;
proxy_cache_valid 200 301 302 10m;
proxy_cache_valid 404 1m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on; # collapse concurrent misses into one upstream fetch
add_header X-Cache-Status $upstream_cache_status; # HIT / MISS / EXPIRED — great for debugging
}
}
Notes on the config:
X-Forwarded-For/X-Forwarded-Protoare essential so the app sees the real client IP and scheme after TLS termination. Make sure your framework is configured to trust these headers only from the proxy.proxy_cache_lock onprevents a “thundering herd” of identical requests hitting the backend when a popular entry expires.X-Cache-Statussurfaces whether a response was served from cache — invaluable when tuning. See Caching for cache-key and invalidation strategy.proxy_cache_use_staleimproves resilience: serve slightly stale content if the backend is momentarily down.
Reload without dropping connections after editing config:
nginx -t # validate config first — never reload a broken config
nginx -s reload # graceful reload: new workers start, old ones drain
Tuning basics
| Setting | Guidance |
|---|---|
worker_processes | Set to auto (one per CPU core) for CPU-bound TLS/compression work |
worker_connections | Raise (e.g. 1024–65535) alongside the OS file-descriptor limit (ulimit -n) |
keepalive_timeout | 60–75s for clients; keep an upstream keepalive pool to reduce reconnects |
| Timeouts | Set client and proxy read/send timeouts to bound resource usage |
sendfile / tcp_nopush | Enable for efficient static file delivery |
| Logging | Use structured/buffered access logs; disable access log for high-volume static paths |
Operational best practices
- Terminate TLS at the edge and keep certificates in one place; automate renewal (Certbot, or Caddy’s built-in ACME).
- Always
nginx -tbefore reload, and use graceful reloads to avoid dropped requests during deploys. - Let the web server serve static assets; only proxy dynamic requests to the app.
- Set and forward the right headers (
X-Forwarded-*) and add security headers (HSTS,nosniff). - Cache deliberately: cache what is safe, key correctly, expose
X-Cache-Status, and have an invalidation plan. - Rate-limit and buffer to protect app workers from abuse and slow clients.
- Health-check upstreams and configure
max_fails/fail_timeoutso traffic avoids dead instances. - Match timeouts to reality: streaming/SSE endpoints need buffering off and longer read timeouts.
- Run multiple proxy instances behind a load balancer for the proxy tier’s own availability — see Scalability and Reliability.