Reverse Proxy Explained: How It Works and Why You Need One (2026)
A reverse proxy sits between your users and backend servers — handling SSL termination, load balancing, caching, and DDoS protection. Covers Nginx, Caddy, Traefik, and cloud-native setups.
Your backend service listens on port 8080. It speaks plain HTTP. It has no SSL certificate. It crashes under load. And it exposes your server's IP address directly to the public internet.
A reverse proxy fixes all of this — without touching your application code.
A reverse proxy is a server that sits in front of your backend services, intercepting all incoming requests and forwarding them on behalf of clients. From the client's perspective, it's talking directly to your application. In reality, it's talking to the proxy, which decides where to route the request, optionally caches the response, terminates TLS, and forwards only validated traffic to your backend.
Nginx, Caddy, Traefik, HAProxy, Envoy — these are all reverse proxies. So is Cloudflare, conceptually. They all do the same fundamental thing: intercept, inspect, and forward requests to backend services while handling cross-cutting concerns that would otherwise pollute every service you write.
What Is a Reverse Proxy?
A reverse proxy accepts client connections and proxies them to one or more upstream servers. The client sends its request to the proxy's IP address, not to your application's IP address. The proxy forwards the request to the appropriate backend, receives the response, and returns it to the client.
Client
│
▼
Reverse Proxy (port 443)
│ ├── terminates TLS
│ ├── checks rate limits
│ ├── strips internal headers
│ └── selects upstream server
│
├──▶ Backend A (port 8080) — app server 1
├──▶ Backend B (port 8081) — app server 2
└──▶ Backend C (port 8082) — static assets / API v2Your backend servers are never exposed directly to the internet. They live on a private network, reachable only from the proxy. This is the foundational security model for nearly every production web architecture.
What Problem Does It Solve?
Without a reverse proxy, you'd need every backend service to independently handle:
- TLS certificate acquisition and renewal
- DDoS and rate limiting
- Request routing by path or hostname
- Load balancing across multiple instances
- Compression (gzip/brotli) for every response
- Access logging in a consistent format
- Health checking and failover
A reverse proxy centralizes all of this. Your Node.js app, your Python API, your Go microservice — they all just return HTTP responses. The proxy handles the rest.
Forward Proxy vs Reverse Proxy
These terms confuse people. The distinction is about whose interests the proxy serves.
Forward proxy: Sits in front of clients and acts on their behalf. Clients are configured to route traffic through it. Common uses: corporate internet filtering, anonymizing client identity, caching outbound requests. The server receiving traffic sees the proxy's IP, not the client's.
Reverse proxy: Sits in front of servers and acts on their behalf. Clients don't know it exists — they think they're talking to the application. Common uses: load balancing, SSL termination, DDoS protection. The backend receiving traffic sees the proxy forwarding the request.
Forward Proxy (client side):
Client → [Forward Proxy] → Internet → Server
↑
clients configure this
Reverse Proxy (server side):
Client → [Reverse Proxy] → Backend Servers
↑
servers sit behind thisA VPN or SOCKS proxy is a forward proxy. Nginx in front of your app servers is a reverse proxy. Cloudflare is technically a reverse proxy — you point your DNS to Cloudflare's edge, and it proxies traffic to your origin.
Core Functions of a Reverse Proxy
SSL/TLS Termination
Your backend services speak plain HTTP. The reverse proxy handles TLS — certificate management, TLS handshakes, and cipher negotiation — at the edge, then forwards decrypted HTTP internally.
# Nginx: terminate TLS at the proxy, forward plain HTTP internally
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
# Only allow TLS 1.2 and 1.3 — no SSLv3, TLSv1.0, TLSv1.1
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# HSTS — tell browsers to always use HTTPS for 1 year
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
proxy_pass http://127.0.0.1:8080; # Plain HTTP to 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;
}
}
# Redirect HTTP to HTTPS
server {
listen 80;
server_name api.example.com;
return 301 https://$host$request_uri;
}Benefits: Your backend never touches a certificate. When Let's Encrypt renews the cert, you reload Nginx — the app doesn't restart. You configure cipher suites once, in one place, for all backends.
The X-Forwarded-For header is critical: because the backend sees the proxy's IP, not the real client IP, the proxy adds this header so your application can log the actual user IP address and do accurate rate limiting.
Load Balancing
A reverse proxy distributes incoming requests across multiple backend instances. This is how you scale horizontally — add more app servers; the proxy spreads load across them.
# Nginx upstream block — defines the pool of backend servers
upstream app_servers {
# Round-robin by default (each request goes to the next server in order)
server 10.0.0.1:8080;
server 10.0.0.2:8080;
server 10.0.0.3:8080;
# Or use least_conn — send each request to the server with fewest active connections
# least_conn;
# Or ip_hash — same client always goes to the same server (sticky sessions)
# ip_hash;
# Mark a server as backup — only used when all primaries are down
server 10.0.0.4:8080 backup;
keepalive 32;
}
server {
listen 443 ssl http2;
location / {
proxy_pass http://app_servers;
proxy_next_upstream error timeout http_502 http_503; # Retry on failure
}
}Load balancing algorithms:
| Algorithm | How It Works | Best For |
|---|---|---|
| Round-robin | Rotates through backends sequentially | Stateless apps with homogeneous servers |
| Least connections | Sends to server with fewest active requests | Long-lived connections (WebSockets) |
| IP hash | Same client IP always → same backend | Stateful apps requiring sticky sessions |
| Weighted | Some servers get more traffic (e.g., 3:1 ratio) | Heterogeneous server capacity |
| Random | Random selection | High-throughput, short requests |
Request Routing
A reverse proxy routes requests to different backends based on URL path or hostname. This is how microservices architecture works in practice — one public URL, multiple backend services.
# Route by path — monolith vs microservices hybrid
server {
listen 443 ssl http2;
server_name example.com;
# API v1 — legacy monolith
location /api/v1/ {
proxy_pass http://monolith:8080;
}
# API v2 — new microservices
location /api/v2/users/ {
proxy_pass http://user-service:3001;
}
location /api/v2/orders/ {
proxy_pass http://order-service:3002;
}
location /api/v2/payments/ {
proxy_pass http://payment-service:3003;
}
# Static assets — served directly by Nginx or forwarded to a CDN origin
location /static/ {
root /var/www;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Default — frontend app
location / {
proxy_pass http://frontend:3000;
}
}Virtual hosting — route by hostname to different apps on the same server:
# Same IP, different server_name blocks, different backends
server {
server_name app.example.com;
location / { proxy_pass http://main-app:8080; }
}
server {
server_name admin.example.com;
location / { proxy_pass http://admin-app:8081; }
}
server {
server_name api.example.com;
location / { proxy_pass http://api-service:3000; }
}Compression
Compressing responses at the proxy means your backend doesn't have to. One configuration covers all services.
# Nginx gzip compression — enable globally for all proxied responses
gzip on;
gzip_vary on;
gzip_min_length 1024; # Don't compress tiny responses (overhead > savings)
gzip_proxied any; # Compress proxied responses too
gzip_comp_level 6; # 1 (fastest) to 9 (best compression) — 6 is the sweet spot
gzip_types
text/plain
text/css
text/javascript
application/json
application/javascript
application/x-javascript
application/xml
image/svg+xml;
# Note: never gzip already-compressed formats (jpeg, png, gif, mp4, zip)Brotli achieves 15–25% better compression than gzip on text content. Nginx requires the ngx_brotli module (available in mainline nginx from 1.27+, or via nginx-mod-http-brotli):
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/json application/javascript text/xml image/svg+xml;Caching
A reverse proxy can cache backend responses, reducing the number of requests that reach your application — identical to how a CDN works, but running on your own infrastructure.
# Define a shared memory zone for cache metadata and a disk path for cache files
proxy_cache_path /var/cache/nginx
levels=1:2 # Directory nesting (avoids too many files in one dir)
keys_zone=api_cache:10m # 10MB for cache keys in shared memory
max_size=1g # Max 1GB of cached responses on disk
inactive=60m # Remove cached items not accessed for 60 minutes
use_temp_path=off;
server {
location /api/products {
proxy_pass http://backend:8080;
proxy_cache api_cache;
proxy_cache_valid 200 5m; # Cache 200 responses for 5 minutes
proxy_cache_valid 404 1m; # Cache 404s for 1 minute (negative caching)
proxy_cache_key "$scheme$request_method$host$request_uri";
add_header X-Cache-Status $upstream_cache_status;
}
}The X-Cache-Status header lets you verify caching behavior:
HIT— served from Nginx cacheMISS— fetched from backendEXPIRED— was cached but TTL elapsedBYPASS— cache explicitly bypassed (e.g.,Cache-Control: no-cachein request)
Security Headers
Add security headers at the proxy once, rather than in every application.
server {
# Remove headers that leak server information
server_tokens off;
more_clear_headers Server; # Requires ngx_headers_more module
# Security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
location / {
proxy_pass http://backend:8080;
# Strip headers the backend shouldn't be able to spoof
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Prevent internal headers from being set by clients
proxy_set_header X-Internal-Secret ""; # Clear any client-sent internal header
}
}Nginx: The Standard Reverse Proxy
Nginx is the most widely deployed reverse proxy and web server. It handles millions of concurrent connections with minimal memory because it uses an event-driven, non-blocking architecture — a single Nginx worker can handle tens of thousands of open connections simultaneously.
Nginx Configuration Structure
/etc/nginx/
├── nginx.conf # Main config — worker processes, events block, http block
├── conf.d/ # Include all *.conf files from here
│ ├── default.conf
│ └── api.conf
└── sites-enabled/ # Symlinked from sites-available/ (Debian/Ubuntu pattern)# /etc/nginx/nginx.conf — top-level config
worker_processes auto; # One worker per CPU core
worker_rlimit_nofile 65535; # Max open files per worker
events {
worker_connections 4096; # Max connections per worker (total = workers × connections)
use epoll; # Linux event model — most efficient on Linux
multi_accept on; # Accept all pending connections at once
}
http {
include mime.types;
default_type application/octet-stream;
# Logging format includes real client IP (from X-Real-IP), not proxy IP
log_format main '$http_x_real_ip - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
# Performance settings
sendfile on; # Send files directly from disk to socket (zero-copy)
tcp_nopush on; # Send full TCP packets (pairs with sendfile)
tcp_nodelay on; # Disable Nagle algorithm for low-latency responses
keepalive_timeout 65; # HTTP keep-alive timeout
# Connection timeouts to backend
proxy_connect_timeout 5s; # How long to wait for backend connection
proxy_send_timeout 60s; # How long to wait sending request to backend
proxy_read_timeout 60s; # How long to wait for backend response
include /etc/nginx/conf.d/*.conf;
}Production Nginx Proxy Config
# /etc/nginx/conf.d/api.conf
upstream api_backend {
least_conn;
server 10.0.1.1:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.2:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.3:8080 max_fails=3 fail_timeout=30s;
keepalive 64; # Maintain persistent connections to backends
}
server {
listen 80;
server_name api.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Rate limiting (define zones in http block)
limit_req zone=api_limit burst=20 nodelay;
location /api/ {
proxy_pass http://api_backend;
# Pass real client info to 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;
# Use HTTP/1.1 to backends (enables keep-alive)
proxy_http_version 1.1;
proxy_set_header Connection "";
# Buffer settings — balance between memory and latency
proxy_buffering on;
proxy_buffer_size 16k;
proxy_buffers 8 16k;
proxy_busy_buffers_size 32k;
}
# Health check endpoint — proxied directly, no rate limiting
location /health {
proxy_pass http://api_backend;
access_log off; # Don't log health checks
}
}Rate Limiting in Nginx
http {
# Define shared memory zones for rate limit state
# 10m = 10MB of shared memory, enough for ~160,000 IP addresses
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/m; # 100 req/min per IP
limit_req_zone $binary_remote_addr zone=login_limit:5m rate=5r/m; # 5 req/min for login
server {
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
# burst=20: allow 20 excess requests (queued), then 429
# nodelay: process burst immediately rather than metering
limit_req_status 429;
proxy_pass http://backend;
}
location /api/auth/login {
limit_req zone=login_limit burst=3 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
}
}Caddy: Automatic HTTPS
Caddy is a modern reverse proxy written in Go. Its killer feature: automatic HTTPS. Caddy automatically obtains and renews Let's Encrypt certificates for every domain it serves. You never manage a certificate.
# Caddyfile — minimal reverse proxy with automatic HTTPS
api.example.com {
reverse_proxy localhost:8080
}That's the entire config. Caddy handles DNS-validated HTTPS, HTTP to HTTPS redirects, certificate renewal, and HTTP/2. Compare this to 40+ lines of Nginx SSL configuration.
# Caddyfile — production setup with multiple services
api.example.com {
# Route to different backends by path
handle /v2/* {
reverse_proxy user-service:3001 order-service:3002 {
lb_policy least_conn
health_uri /health
health_interval 10s
}
}
handle /v1/* {
reverse_proxy monolith:8080
}
# Rate limiting (requires caddy-ratelimit plugin)
rate_limit {
zone api {
key {remote_host}
events 100
window 1m
}
}
# Compression
encode gzip zstd
# Security headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options nosniff
X-Frame-Options DENY
-Server # Remove Server header
}
# Logging
log {
output file /var/log/caddy/access.log
format json
}
}
admin.example.com {
reverse_proxy admin-app:8081
# Restrict to office IP range
@blocked not remote_ip 203.0.113.0/24
respond @blocked 403
}Caddy is a strong choice for: new projects where simplicity matters, teams without dedicated ops who don't want to manage certificates, and Docker environments where a single container handles TLS.
Traefik: Container-Native Reverse Proxy
Traefik is designed for dynamic container environments. Instead of static config files, Traefik discovers services automatically by watching Docker, Kubernetes, or Consul for container labels and annotations.
# docker-compose.yml — Traefik with automatic service discovery
version: "3.8"
services:
traefik:
image: traefik:v3.0
command:
- "--providers.docker=true" # Watch Docker for services
- "--providers.docker.exposedbydefault=false" # Only expose labeled containers
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.le.acme.email=ops@example.com"
- "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
- "--certificatesresolvers.le.acme.tlschallenge=true"
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro" # Read Docker events
- "./letsencrypt:/letsencrypt"
api:
image: my-api:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(`api.example.com`) && PathPrefix(`/api`)"
- "traefik.http.routers.api.entrypoints=websecure"
- "traefik.http.routers.api.tls.certresolver=le"
- "traefik.http.services.api.loadbalancer.server.port=8080"
- "traefik.http.middlewares.api-ratelimit.ratelimit.average=100"
- "traefik.http.middlewares.api-ratelimit.ratelimit.burst=20"
frontend:
image: my-frontend:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.frontend.rule=Host(`example.com`)"
- "traefik.http.routers.frontend.entrypoints=websecure"
- "traefik.http.routers.frontend.tls.certresolver=le"
- "traefik.http.services.frontend.loadbalancer.server.port=3000"When a new container starts with the traefik.enable=true label, Traefik detects it and starts routing traffic to it — zero config file changes, zero restarts. When the container stops, Traefik stops routing to it.
This makes Traefik the default choice for Docker Compose and Kubernetes setups where services are ephemeral and configs need to be dynamic.
Reverse Proxy vs Load Balancer vs API Gateway
These terms overlap significantly. Understanding where they differ prevents architectural confusion.
| Concern | Reverse Proxy | Load Balancer | API Gateway |
|---|---|---|---|
| SSL termination | Yes | Sometimes | Yes |
| Request routing (path/host) | Yes | No (usually L4 only) | Yes |
| Load distribution | Yes (Layer 7) | Yes (L4 or L7) | Yes |
| Authentication / authorization | No | No | Yes |
| Rate limiting | Yes (basic) | No | Yes (advanced) |
| Request/response transformation | Limited | No | Yes |
| Service discovery integration | Traefik, yes | No | Yes |
| Example products | Nginx, Caddy, Traefik | AWS NLB, HAProxy L4 | Kong, AWS API GW, Apigee |
Use a reverse proxy when you need SSL, routing, load balancing, and compression. That's most applications.
Use a dedicated load balancer (like AWS Network Load Balancer) when you need pure L4 TCP/UDP load balancing with extreme throughput and minimal latency overhead — e.g., in front of a cluster of reverse proxies.
Use an API gateway when you need authentication enforcement, per-consumer rate limiting, request transformation, developer portal / API key management, or multi-version API management. An API gateway is typically a reverse proxy with business logic baked in.
In a typical production architecture:
Internet
│
▼
AWS NLB (L4, routes TCP to multiple AZs)
│
▼
Nginx / Traefik cluster (L7, TLS, routing, rate limiting)
│
├──▶ API Gateway (Kong) — authenticated /api/* routes
│
└──▶ Backend services — unauthenticated or service-to-service routesHealth Checks and Failover
A reverse proxy only improves availability if it detects and routes around failures. Passive health checks notice failures from real traffic; active health checks probe backends proactively.
# Nginx passive health checks — built into open-source Nginx
upstream backend {
server 10.0.1.1:8080 max_fails=3 fail_timeout=30s;
# If 3 requests fail within 30 seconds, mark server as down for 30 seconds
# After 30 seconds, try it again with 1 request — if it succeeds, restore it
}In Traefik, health checks are configured per service:
labels:
- "traefik.http.services.api.loadbalancer.healthcheck.path=/health"
- "traefik.http.services.api.loadbalancer.healthcheck.interval=10s"
- "traefik.http.services.api.loadbalancer.healthcheck.timeout=3s"Your backend's /health endpoint should check its own dependencies — database connectivity, cache availability — not just return 200. A server that can't reach the database should fail its health check so the proxy stops sending it traffic.
WebSocket Proxying
HTTP proxying doesn't work for WebSockets out of the box. WebSocket connections start as HTTP and then upgrade — the proxy must understand and support this.
# Nginx WebSocket configuration
upstream ws_backend {
server ws-service:8080;
keepalive 64;
}
server {
location /ws/ {
proxy_pass http://ws_backend;
# Required for WebSocket upgrade
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Extended timeouts — WebSocket connections are long-lived
proxy_read_timeout 3600s; # 1 hour
proxy_send_timeout 3600s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}The Upgrade and Connection headers tell both the proxy and the backend to switch protocols. Without these headers, the proxy treats the connection as regular HTTP and the WebSocket handshake fails with a 400 or 101 that the proxy then closes prematurely.
If you're using load balancing with WebSockets, use ip_hash or least_conn (not round-robin) — WebSocket sessions are long-lived connections that should stay pinned to one backend.
Timeouts: The Silent Killer
Incorrect timeout configuration causes two failure modes:
- Too short: Legitimate slow requests fail (file uploads, report generation, long queries)
- Too long: Failed backends tie up connections, cascading into full connection pool exhaustion
# Nginx timeout hierarchy — each has a distinct purpose
proxy_connect_timeout 5s; # Max time to establish connection to backend
# If backend doesn't accept in 5s, fail immediately
proxy_send_timeout 30s; # Max time between writes when sending request to backend
# Not total request time — time between TCP writes
proxy_read_timeout 60s; # Max time between reads when receiving from backend
# Not total response time — time between TCP reads
# For long-running operations (file uploads, reports):
location /api/reports/ {
proxy_read_timeout 300s; # Allow 5 minutes for report generation
proxy_send_timeout 120s; # Allow 2 minutes to send large request body
client_max_body_size 50m; # Allow 50MB request body (for file uploads)
proxy_pass http://backend;
}A common pattern is a tiered timeout strategy: aggressive short timeouts for regular API endpoints (fail fast), generous timeouts for known slow operations (uploads, reports, batch jobs), and circuit breaker logic at the application layer to stop hammering a slow backend.
Observability
A reverse proxy is the ideal place to generate access logs — it sees every request, regardless of which backend serves it.
# JSON access log format — compatible with log aggregators (Elasticsearch, Datadog, Grafana Loki)
log_format json_combined escape=json
'{'
'"time":"$time_iso8601",'
'"remote_addr":"$http_x_real_ip",' # Real client IP (not proxy)
'"method":"$request_method",'
'"uri":"$request_uri",'
'"status":$status,'
'"bytes_sent":$bytes_sent,'
'"request_time":$request_time,' # Total time from request to response
'"upstream_response_time":"$upstream_response_time",' # Backend processing time
'"upstream_addr":"$upstream_addr",' # Which backend server handled it
'"http_referer":"$http_referer",'
'"http_user_agent":"$http_user_agent"'
'}';
access_log /var/log/nginx/access.log json_combined;$upstream_response_time is particularly valuable: it's the backend's processing time, excluding network time between client and proxy. If $request_time is high but $upstream_response_time is low, the bottleneck is network (client is slow). If both are high, the backend is slow.
Reverse Proxy in Kubernetes
In Kubernetes, the reverse proxy is typically an Ingress Controller — a reverse proxy (usually Nginx or Traefik) that watches Kubernetes Ingress resources and configures itself automatically.
# Kubernetes Ingress — declarative routing config
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/limit-rps: "100" # Rate limiting annotation
nginx.ingress.kubernetes.io/proxy-body-size: "50m" # Max request body
cert-manager.io/cluster-issuer: "letsencrypt-prod" # Automatic TLS via cert-manager
spec:
ingressClassName: nginx
tls:
- hosts:
- api.example.com
secretName: api-tls-secret # cert-manager populates this
rules:
- host: api.example.com
http:
paths:
- path: /api/v2/users
pathType: Prefix
backend:
service:
name: user-service
port:
number: 80
- path: /api/v2/orders
pathType: Prefix
backend:
service:
name: order-service
port:
number: 80
- path: /
pathType: Prefix
backend:
service:
name: frontend
port:
number: 3000The Nginx Ingress Controller watches for changes to Ingress resources and automatically updates its routing configuration. Add a new microservice: create an Ingress resource; traffic starts routing within seconds, with TLS handled automatically by cert-manager.
Frequently Asked Questions
What is the difference between a reverse proxy and a load balancer?
A reverse proxy operates at Layer 7 (HTTP) and can make routing decisions based on URLs, headers, and request content. It handles SSL termination, compression, caching, and request routing to multiple different backend services. A load balancer in the traditional sense operates at Layer 4 (TCP/UDP) and distributes connections across identical servers based purely on network-level information — it doesn't inspect HTTP content. In practice, modern tools blur this: Nginx is a reverse proxy that also load balances, while AWS ALB is a load balancer that also proxies HTTP. The key distinction is Layer 4 (connection distribution) vs Layer 7 (HTTP-aware routing).
Why use Nginx instead of letting each service handle its own SSL?
SSL termination at the proxy centralizes certificate management, cipher suite configuration, and TLS version enforcement in one place. If you have 10 backend services and manage certificates individually, you have 10 places to update when a cipher is deprecated, 10 renewal schedules to track, and 10 configurations to audit. The proxy does it once. Backend services speak plain HTTP on a private network, which is simpler and faster (no TLS overhead on internal connections). It also lets you configure HTTP/2 once at the proxy — backends can remain on HTTP/1.1 internally.
What is the difference between a reverse proxy and an API gateway?
A reverse proxy handles infrastructure concerns: TLS termination, routing, load balancing, compression, and caching. It doesn't understand your application's business logic. An API gateway adds application-layer concerns on top: authentication enforcement (validate JWT tokens, API keys), per-consumer rate limiting (different limits per client), request transformation (add/remove headers, rewrite request bodies), response transformation, protocol translation (REST to gRPC), and developer portal features (API key issuance, documentation). Many organizations use both: Nginx as the ingress point, Kong or AWS API Gateway behind it for API-specific concerns.
Does a reverse proxy hide my backend IP address?
Yes. If configured correctly, client connections terminate at the proxy. Your backend servers are on a private network with no public IP address, or have firewall rules blocking all inbound traffic except from the proxy's IP. An attacker scanning the internet sees only the proxy's IP. This is a critical security benefit — direct attacks on your application servers are impossible if they're unreachable from the public internet. Combine this with restricting SSH access to a bastion host and you've eliminated most network-level attack surface.
Should I use Nginx, Caddy, or Traefik?
Nginx: Most battle-tested, widest ecosystem, best for high-traffic production workloads where you need fine-grained tuning. Steeper config learning curve. Default choice for bare-metal and VM deployments.
Caddy: Best for teams who want zero-config HTTPS. Automatically provisions and renews Let's Encrypt certificates. Simpler config syntax. Excellent for new projects and smaller teams. Go-native, easy to extend with plugins.
Traefik: Best for Docker and Kubernetes environments. Auto-discovers services from container labels and Kubernetes annotations without config file changes. Built-in dashboard for observability. Less performant than Nginx under extreme load, but fine for most workloads.
How does a reverse proxy improve security?
A reverse proxy improves security in several ways: (1) It hides backend server IPs, preventing direct attacks. (2) It strips internal headers that clients shouldn't be able to set (like X-Internal-Role). (3) It adds security response headers (CSP, HSTS, X-Frame-Options) uniformly across all services. (4) It enables rate limiting to slow brute-force and DDoS attacks. (5) It terminates TLS, ensuring clients can't negotiate weak cipher suites even if a backend misconfigures it. (6) It centralizes access logging, giving you a single audit trail for all requests. None of these require changes to backend code.
What is X-Forwarded-For and why does it matter?
When a reverse proxy forwards a request to the backend, the backend sees the proxy's IP address as the "remote address" — not the real client's IP. X-Forwarded-For is a header the proxy adds to pass along the original client IP. Without it, your application can't do per-IP rate limiting, geolocation, fraud detection, or accurate logging. Your application must trust and read this header, but only from known proxy IPs — if clients can send their own X-Forwarded-For header and your app trusts it, they can spoof their IP address. In Nginx, proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for appends the real IP to any existing forwarded chain, preserving multi-hop tracing.
Key Takeaways
- A reverse proxy intercepts all client traffic before it reaches backend servers — centralizing SSL, routing, load balancing, compression, caching, and security headers
- SSL termination at the proxy means backend services speak plain HTTP on private networks; certificate management is centralized in one place
- Nginx is the standard choice for high-traffic production; Caddy for zero-config HTTPS; Traefik for container-native auto-discovery
- The
X-Forwarded-Forheader is essential — without it, backends see the proxy's IP, not the real client IP, breaking rate limiting and logging - Load balancing at Layer 7 (HTTP) lets you route by URL path, hostname, or header — not just distribute connections round-robin
- Health checks (passive or active) enable automatic failover; without them, the proxy sends requests to dead backends
- WebSocket proxying requires explicit
UpgradeandConnectionheaders; without them, the upgrade handshake fails - A reverse proxy is the natural place for access logging — it sees every request across all services in a consistent format
Related reading: Load Balancing Strategies · CDN Explained · Rate Limiting Your API · Service Discovery Explained
Enjoyed this article?
Get weekly insights on backend architecture, system design, and Go programming.
Related Posts
Continue reading with these related posts
DNS (Domain Name System) Explained: How It Works, Record Types, Security, and Cloud
Complete guide to how DNS works: resolution process, record types, TTL, DNSSEC, DNS over HTTPS, Route 53, Cloudflare, troubleshooting with dig, and common DNS mistakes engineers make.
Load Balancing Explained: Algorithms, Layers, and Strategies
Load balancing distributes traffic across servers so no single machine becomes a bottleneck. Learn every major algorithm — round robin, least connections, IP hash, and more — plus Layer 4 vs Layer 7, health checks, sticky sessions, and real nginx/HAProxy config examples.
API Gateway Pattern: The Front Door to Your Microservices
What is an API gateway and what does it do? Complete guide covering routing, authentication, rate limiting, Node.js implementation, BFF pattern, and comparisons of Kong, AWS API Gateway, nginx, and Traefik.