
Forward vs Reverse Proxy
Who is hiding? User (Forward) or Server (Reverse)?

Who is hiding? User (Forward) or Server (Reverse)?
Why does my server crash? OS's desperate struggle to manage limited memory. War against Fragmentation.

Two ways to escape a maze. Spread out wide (BFS) or dig deep (DFS)? Who finds the shortest path?

A comprehensive deep dive into client-side storage. From Cookies to IndexedDB and the Cache API. We explore security best practices for JWT storage (XSS vs CSRF), performance implications of synchronous APIs, and how to build offline-first applications using Service Workers.

Fast by name. Partitioning around a Pivot. Why is it the standard library choice despite O(N²) worst case?

When I was a junior developer, my senior casually mentioned:
"You use VPN, right? That's a proxy."
A few days later, the same senior said:
"I just put Nginx in front of our servers. That's also a proxy."
Me: "Wait... both are proxies but they have different names?"
He just smiled and said, "Look it up." I did, but all I found were diagrams and explanations repeating "client-side vs server-side" without helping me truly understand. It didn't click.
My question back then was simple: "If both sit in the middle, aren't they fundamentally the same thing?"
When building infrastructure at work, this concept kept popping up:
Every situation involved "proxy," but sometimes we used Forward Proxy and sometimes Reverse Proxy. Not understanding the difference clearly made me confused during design discussions, and especially during debugging when questions like "Where did this IP come from?" left me stumped.
Eventually, I made a mistake. While building an internal company API, I tried to get the client IP on the backend server (behind a Reverse Proxy) using req.connection.remoteAddress, but it always showed the Nginx IP. My senior told me, "You need to check the X-Forwarded-For header." That's when it clicked: "Oh, because Reverse Proxy sends requests on behalf of clients, the original IP gets stored in the header."
The most confusing part: both act as "agents," so why different names? I understood later, but the key was: "Whose agent are they?"
My senior's analogy changed everything:
"Ah, it's about whose side they're on!""Imagine you're in court.
Forward Proxy (lawyer beside me): Me: 'Your Honor, I'm innocent!' Lawyer: '(interrupting) What my client means to say is...' → Speaks for me (protects my identity)
Reverse Proxy (lawyer beside the opponent): Me: 'Defendant, answer!' Lawyer: '(interrupting) My client won't respond directly. Let me answer.' → Shields the opponent (protects server identity)"
Everything fell into place. Forward Proxy is an agent hired by the client, and Reverse Proxy is an agent hired by the server. Same "agent" role, but different employers mean different protected parties, different purposes, and different configurations.
[User] ← here → [Forward Proxy] → [Internet] → [Server]
Forward Proxy sits right beside the user, like a personal assistant.
The key point: the server has no idea the Forward Proxy exists. From the server's perspective, it's simply "a request came from the proxy IP." Whether there are 100 employees or 1000 students behind the proxy, the server can't tell.
The core concept I understood: "The client has control." The client configures the proxy and decides "go through the proxy for this site" or "connect directly for that one."
At work:
Employee PC → Company Forward Proxy → Internet
Scenario:
At our setup, SNS access was blocked during work hours. I initially wondered, "How do they block it?" Turns out the Forward Proxy managed a domain blacklist.
When I wanted to watch US Netflix from Korea:
My PC (Korea) → VPN Server (US) → Netflix (US)
Process:
What was interesting: I always thought VPN was just an "encryption tool," but it's actually a type of Forward Proxy. VPN doesn't just encrypt; it also acts as a proxy sending traffic on my behalf.
# /etc/squid/squid.conf
# Port configuration
http_port 3128
# ACL definitions
acl localnet src 192.168.1.0/24
acl blocked_sites dstdomain .facebook.com .youtube.com .instagram.com
acl work_hours time MTWHF 09:00-18:00
# Rules
http_access deny blocked_sites work_hours
http_access allow localnet
http_access deny all
# Logging
access_log /var/log/squid/access.log squid
What this configuration means:
When I actually configured Squid, the logs showed hundreds of blocked requests per day—employees trying to access Facebook. This is the core function of Forward Proxy: outbound traffic control.
As a side note, Forward Proxy was also useful when I did a web scraping project:
# Python requests with proxy rotation
import requests
proxies = [
'http://proxy1.com:8080',
'http://proxy2.com:8080',
'http://proxy3.com:8080',
]
for i, url in enumerate(target_urls):
proxy = proxies[i % len(proxies)]
response = requests.get(url, proxies={'http': proxy, 'https': proxy})
Scraping from the same IP repeatedly gets you blocked, so I used multiple Forward Proxies, rotating IPs for each request. Another use case for Forward Proxy.
[User] → [Internet] → [Reverse Proxy] ← here → [Backend Servers]
Reverse Proxy sits right in front of the server, like a bodyguard.
The key point: clients have no idea the Reverse Proxy exists. From the client's perspective, "I sent a request to api.company.com and got a response." They have no clue whether there's 1 server or 100 behind it, or whether Nginx or HAProxy is in the middle.
The core concept I understood: "The server has control." The server admin configures the proxy, and clients just send requests normally.
When I built our setup API servers:
User → Nginx (Reverse Proxy)
├→ API Server 1 (Node.js)
├→ API Server 2 (Node.js)
└→ API Server 3 (Node.js)
Process:
What amazed me when building this architecture: even when adding or removing servers, client code never needed changes. Just update Nginx config. This is the core advantage of Reverse Proxy.
HTTPS handling centralized at Nginx:
User (HTTPS) → Nginx (handles HTTPS)
↓ (HTTP)
Backend (HTTP only)
Benefits:
When I actually applied this, the Node.js server's CPU usage dropped about 15%. SSL handshakes consume significant CPU. Offloading that to Nginx meant the backend only handled business logic.
# /etc/nginx/nginx.conf
upstream backend {
least_conn; # Distribute to server with least connections
server 192.168.1.101:3000 weight=3; # Node 1 (weight 3)
server 192.168.1.102:3000 weight=2; # Node 2 (weight 2)
server 192.168.1.103:3000 weight=1; # Node 3 (weight 1)
server 192.168.1.104:3000 backup; # Node 4 (backup server)
}
server {
listen 443 ssl http2;
server_name api.company.com;
# SSL configuration
ssl_certificate /etc/ssl/certs/company.crt;
ssl_certificate_key /etc/ssl/private/company.key;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://backend;
# Header configuration (important!)
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;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
Notable points in this configuration:
X-Real-IP, X-Forwarded-For, etc., pass original client information to the backend.Another Reverse Proxy application: microservices routing
server {
listen 80;
server_name api.company.com;
# User service
location /api/users {
proxy_pass http://user-service:3001;
}
# Payment service
location /api/payments {
proxy_pass http://payment-service:3002;
}
# Notification service
location /api/notifications {
proxy_pass http://notification-service:3003;
}
}
Clients only need to know api.company.com, and Nginx internally routes to the appropriate microservice. This was the core concept I accepted: Reverse Proxy is the traffic director in front of servers.
My actual architecture:
Employee PC → [Company Forward Proxy] → Internet → [AWS]
↓
[Nginx Reverse Proxy]
↓
[API Servers 1, 2, 3]
Scenario (request flow):
Advantages of this structure:
When I first designed this structure, it seemed complex, but in operation, each layer had a clearly separated role, making management easier.
| Item | Forward Proxy | Reverse Proxy |
|---|---|---|
| Position | Beside client | Beside server |
| Hides | Client IP | Server IP/structure |
| Who configures | User/Company | Server admin |
| Purpose | Access control, anonymity, bypass | Load balancing, security, caching |
| Examples | Squid, VPN, Tor, corporate network | Nginx, HAProxy, CloudFlare, AWS ALB |
| Server sees | Proxy IP | Actual client IP (via headers) |
| Client sees | Actual server IP | Proxy IP |
| Configuration location | Client browser/OS | Server infrastructure |
| Traffic direction | Outbound control | Inbound control |
My summary: Forward is the outbound gatekeeper, Reverse is the inbound gatekeeper.
Server logs show:
[2025-05-12 14:30:15] Access from 203.255.1.100 (company proxy IP)
[2025-05-12 14:30:16] Access from 203.255.1.100 (company proxy IP)
[2025-05-12 14:30:17] Access from 203.255.1.100 (company proxy IP)
In reality, Employee A (192.168.1.50), Employee B (192.168.1.51), and Employee C (192.168.1.52) each connected separately, but the server sees them all as the same IP.
This is Forward Proxy's purpose: protecting client identity.
Nginx configuration:
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;
Backend logs:
Connection IP: 192.168.1.100 (Nginx IP)
X-Real-IP: 125.50.60.70 (actual client IP)
X-Forwarded-For: 125.50.60.70, 192.168.1.100
Backend code (Node.js):
// Get actual client IP
const getClientIP = (req) => {
return req.headers['x-real-ip'] ||
req.headers['x-forwarded-for']?.split(',')[0] ||
req.connection.remoteAddress;
};
app.get('/api/users', (req, res) => {
const clientIP = getClientIP(req);
console.log(`Request from: ${clientIP}`); // 125.50.60.70
// IP-based rate limiting
const requestCount = ipRateLimiter.get(clientIP) || 0;
if (requestCount > 100) {
return res.status(429).json({ error: 'Too many requests' });
}
// ...business logic
});
This was exactly where I made my first mistake. Using just req.connection.remoteAddress showed only the Nginx IP, making all clients appear to come from the same IP. When I implemented IP-based rate limiting, one user sending many requests blocked everyone.
Lesson: Behind Reverse Proxy, always check the X-Forwarded-For header.
Even more complex case:
Client → CloudFlare → AWS ALB → Nginx → Backend
In this case, X-Forwarded-For:
X-Forwarded-For: 125.50.60.70, 104.16.1.2, 10.0.1.5, 192.168.1.100
(actual client) (CloudFlare) (ALB) (Nginx)
The first IP is the real client IP. Understanding these proxy chains made log analysis much easier.
Attacker (10,000 req/s) → Nginx (Reverse Proxy)
↓ (Rate limiting applied)
(Only 100 pass)
↓
Backend (Survives!)
Nginx configuration:
# Define rate limiting zones
limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=100r/s;
server {
# Regular pages: 10 per second
location / {
limit_req zone=one burst=20 nodelay;
proxy_pass http://backend;
}
# API: 100 per second
location /api/ {
limit_req zone=api burst=200 nodelay;
proxy_pass http://backend;
}
}
Parameter explanation:
When I actually faced a DDoS attack, this configuration saved the backend. Nginx received thousands of requests per second, but only passed 100 per second to the backend.
server {
location / {
# Block SQL Injection
if ($args ~* "union.*select|insert.*into|drop.*table") {
return 403 "Blocked: SQL Injection detected";
}
# Block XSS
if ($args ~* "<script|javascript:|onerror=") {
return 403 "Blocked: XSS attempt detected";
}
# Block Path Traversal
if ($uri ~* "\.\./") {
return 403 "Blocked: Path traversal detected";
}
proxy_pass http://backend;
}
}
This configuration blocked an average of 50 attack attempts per week. The backend never even saw these malicious requests, staying safe.
# Admin page only accessible from office IPs
location /admin {
allow 203.255.1.0/24; # Company IP range
allow 125.50.60.70; # Remote work IP
deny all;
proxy_pass http://backend;
}
This makes the admin page completely inaccessible from the internet. Before writing permission check logic in backend code, block at the infrastructure level first.
# Cache path configuration
proxy_cache_path /var/cache/nginx
levels=1:2
keys_zone=my_cache:10m
max_size=1g
inactive=60m
use_temp_path=off;
server {
location / {
proxy_cache my_cache;
# Caching rules
proxy_cache_valid 200 10m; # 200 responses: 10 minutes
proxy_cache_valid 301 302 1h; # Redirects: 1 hour
proxy_cache_valid 404 1m; # 404: 1 minute
# Cache key
proxy_cache_key "$scheme$request_method$host$request_uri";
# Header for debugging
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://backend;
}
# Exclude specific paths from caching
location /api/realtime {
proxy_cache off;
proxy_pass http://backend;
}
}
Results (my experience):
Especially caching static content (images, CSS, JS) in Nginx meant the backend was almost idle.
Delete cache when content updates:
location ~ /purge(/.*) {
allow 127.0.0.1;
deny all;
proxy_cache_purge my_cache "$scheme$request_method$host$1";
}
Usage:
# Delete cache
curl -X PURGE http://localhost/purge/api/posts/123
This allows immediate cache refresh after content updates.
User → [ALB (Reverse Proxy)]
├→ Target Group 1: EC2 Instances 1, 2, 3
├→ Target Group 2: Lambda Function
└→ Target Group 3: ECS Container
When I configured ALB on AWS:
# Terraform configuration example
resource "aws_lb" "main" {
name = "company-alb"
load_balancer_type = "application"
subnets = [aws_subnet.public_1.id, aws_subnet.public_2.id]
}
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.main.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01"
certificate_arn = aws_acm_certificate.cert.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.backend.arn
}
}
resource "aws_lb_listener_rule" "api" {
listener_arn = aws_lb_listener.https.arn
condition {
path_pattern {
values = ["/api/*"]
}
}
action {
type = "forward"
target_group_arn = aws_lb_target_group.api.arn
}
}
Automatic features:
When I migrated from on-premises Nginx to AWS ALB, management points drastically reduced. Especially manual SSL certificate renewal being handled automatically by ACM was convenient.
User → CloudFlare (200+ data centers worldwide)
↓ (caching, DDoS defense, WAF)
My small server (Korea)
CloudFlare is the ultimate Reverse Proxy:
Features:
Cost: $0/month (free plan)
After adding CloudFlare:
In container environments:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: company-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/rate-limit: "100"
spec:
rules:
- host: api.company.com
http:
paths:
- path: /users
pathType: Prefix
backend:
service:
name: user-service
port:
number: 3000
- path: /payments
pathType: Prefix
backend:
service:
name: payment-service
port:
number: 3000
Kubernetes Ingress is essentially also a Reverse Proxy. Using Nginx Ingress Controller means Nginx operates internally.
Initially, I wondered "Why Forward/Reverse?"
Forward:
Client goes forward, proxy goes instead. Like an errand runner: instead of "I'll go myself," it's "you go for me."
Reverse:
Server receives reverse direction, proxy receives instead. Like a bodyguard: "don't come to me directly, talk to them."
It came down to this: the proxy direction was different.
When I first studied this concept, it was abstract and difficult. Now, just looking at an Nginx config file, I immediately understand "Oh, this is using Reverse Proxy." It made sense after actually using it.
Core summary:
Both are "agents," but whose agent they are was everything.
The metaphor that really resonated with me: imagine you're at a restaurant. Forward Proxy is like a personal assistant who orders food for you (protecting your identity from the waiter). Reverse Proxy is like a maître d' standing between customers and the kitchen (protecting the chefs from direct customer interaction). Same intermediary role, opposite positions.
Understanding this distinction transformed how I approach infrastructure design. Now when faced with a problem, I ask: "Who needs protection here—the client or the server?" That question immediately tells me which proxy type to use. And that clarity, gained through hands-on experience and mistakes, is what made this concept finally click for me.