Sarah's Journey: Understanding Proxies Through Real-World Challenges
The Urgent Call
Sarah stared at her laptop screen, coffee growing cold beside her. It was 2 AM on a Thursday, and her phone wouldn't stop buzzing. The ML model deployment she'd been working on for weeks was finally live, but something was terribly wrong.
"Sarah, we're getting timeout errors on the API," her teammate Mike's message read. "Users in Europe can't access the service at all. The load is killing our single server."
She'd seen this coming. The team had rushed the deployment, and she'd warned them about bottlenecks. Now, with thousands of users trying to access their new image classification API simultaneously, their single backend server was buckling under pressure.
Sarah took a deep breath. She'd heard about proxies solving these exact problems, but had never implemented one herself. Tonight, that was about to change.
First Attempt: The Naive Approach
Sarah's first instinct was simple: just add more backend servers and have the frontend rotate between them. She quickly spun up two additional API servers and modified the client code to randomly select a server.
# client.py - Sarah's first attempt
import random
import requests
API_SERVERS = [
"http://api-server-1.company.com:8080",
"http://api-server-2.company.com:8080",
"http://api-server-3.company.com:8080"
]
def call_api(image_data):
# Randomly pick a server
server = random.choice(API_SERVERS)
response = requests.post(f"{server}/classify", json={"image": image_data})
return response.json()
She deployed the update and watched the metrics. For a brief moment, things seemed better. Then the problems started:
- When
api-server-2went down for a health check, users got cryptic connection errors - Mobile clients had to be updated to use the new logic
- SSL certificates had to be managed on three different servers
- She had no visibility into which server was actually handling requests
- Some servers were getting hammered while others sat idle
"This is chaos," Sarah muttered, rubbing her tired eyes. There had to be a better way.
The Learning Moment: Discovery of Proxies
The next morning, Sarah called her mentor, James, a senior DevOps architect. He laughed when she explained her random server selection approach.
"Sarah, you've just reinvented the wheel, poorly," James said kindly. "What you need is a reverse proxy. Think of it as a smart receptionist for your backend servers."
James explained the concept: instead of clients knowing about multiple backend servers, they'd talk to a single proxy server. The proxy would then intelligently distribute requests to healthy backend servers, handle SSL, and provide a single point of monitoring.
"It's like a load balancer?" Sarah asked.
"Exactly! A reverse proxy can act as a load balancer, among other things," James confirmed. "Let me show you how to set one up with Nginx."
Sarah's mind raced with the possibilities. One entry point, intelligent distribution, centralized SSL, health checks... This was exactly what she needed.
Understanding the Architecture
James sketched out the architecture on their video call:
Internet Users (Worldwide)
↓
[Reverse Proxy - Nginx]
↓ ↓ ↓
[Server1] [Server2] [Server3]
(US-East) (US-West) (EU-West)
"The reverse proxy sits between your users and your backend servers," James explained. "It receives all incoming requests and forwards them to available backend servers using algorithms like round-robin, least connections, or IP hash."
Sarah scribbled notes furiously. This was different from what she'd built in two key ways:
- Single Entry Point: Users only know about one address - the proxy
- Intelligence: The proxy can detect failed servers and avoid sending traffic to them
"There's also the forward proxy," James added, "but that's the opposite - it sits on the client side, making requests on behalf of clients. Think of corporate networks that filter internet access."
Building the Solution
Sarah rolled up her sleeves and started configuring Nginx as a reverse proxy. She created her configuration file carefully:
# /etc/nginx/nginx.conf
http {
# Define upstream backend servers
upstream ml_api_backend {
# Use least_conn algorithm for better distribution
least_conn;
# Define backend servers with health check parameters
server 10.0.1.10:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.11:8080 max_fails=3 fail_timeout=30s;
server 10.0.1.12:8080 max_fails=3 fail_timeout=30s;
}
# Define the proxy server
server {
listen 80;
server_name api.company.com;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name api.company.com;
# SSL configuration (centralized!)
ssl_certificate /etc/nginx/ssl/api.company.com.crt;
ssl_certificate_key /etc/nginx/ssl/api.company.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
# Proxy configuration
location /classify {
proxy_pass http://ml_api_backend;
# Forward important headers
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;
# Timeout settings
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Handle large image uploads
client_max_body_size 10M;
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
}
As Sarah typed each line, she understood what she was building:
- upstream ml_api_backend: Defined her three backend servers as a pool
- least_conn: Ensured requests went to the server with fewest active connections
- max_fails & fail_timeout: Automatically removed unhealthy servers from rotation
- SSL termination: Handled HTTPS at the proxy level, keeping backend servers simple
- Header forwarding: Ensured backend servers knew the real client IP
She tested the configuration syntax:
# Test Nginx configuration
sudo nginx -t
# Output:
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful
Perfect! Now to apply it:
# Reload Nginx with new configuration
sudo systemctl reload nginx
# Check status
sudo systemctl status nginx
The Resolution: Watching It Work
Sarah opened her monitoring dashboard and updated the client code to use the new single endpoint:
# client.py - The elegant solution
import requests
API_ENDPOINT = "https://api.company.com"
def call_api(image_data):
# Simple! Just one endpoint
response = requests.post(f"{API_ENDPOINT}/classify", json={"image": image_data})
return response.json()
She sent a test request. The proxy received it, selected a backend server using the least-connection algorithm, and forwarded the request. The response came back instantly.
She watched as thousands of requests flowed through the system:
- Requests were evenly distributed across all three servers
- When she took
server-2down for maintenance, Nginx automatically stopped sending traffic to it - SSL was managed in one place
- She could see all traffic patterns in Nginx's access logs
- Response times improved by 40%
European users reported the service was now fast and reliable. The timeout errors vanished.
Sarah leaned back in her chair, allowing herself a small smile. The system that had been failing hours ago was now humming along smoothly, handling 10x the traffic without breaking a sweat.
Reflection: What Sarah Learned
As Sarah documented her solution for the team, she reflected on the key lessons:
Understanding Proxy Types
Forward Proxy (Client-side):
- Sits between clients and the internet
- Clients must be configured to use it
- Use cases: Content filtering, bandwidth savings, privacy
- Example: Corporate network proxy for web browsing
Reverse Proxy (Server-side):
- Sits between the internet and backend servers
- Transparent to clients (they don't know it exists)
- Use cases: Load balancing, SSL termination, security, caching
- Example: What Sarah built with Nginx
Key Benefits Realized
- Single Point of Entry: Clients only need to know one address
- Automatic Failover: Unhealthy servers are automatically bypassed
- SSL Termination: Manage certificates in one place instead of on every backend
- Load Distribution: Intelligent algorithms ensure even distribution
- Observability: Centralized logging and monitoring
- Security: Hide backend infrastructure, add rate limiting, filtering
The Implementation Details That Mattered
# The load balancing algorithm choice matters
least_conn; # Route to server with fewest active connections
# vs
# round_robin; # Default - rotate servers sequentially
# vs
# ip_hash; # Same client IP always goes to same server
Sarah realized that choosing least_conn was crucial for her ML API because inference times varied based on image complexity. This algorithm ensured no single server got overwhelmed with slow requests.
Expanding Knowledge: Forward Proxy Example
Curious about the other side of the proxy world, Sarah experimented with a forward proxy. She set up Squid to control and monitor outbound traffic from her development team:
# Install Squid proxy
sudo apt install squid -y
# Basic configuration /etc/squid/squid.conf
http_port 3128
# Define allowed networks
acl localnet src 192.168.1.0/24
# Allow local network
http_access allow localnet
http_access deny all
# Cache configuration
cache_dir ufs /var/spool/squid 1000 16 256
Now developers could configure their browsers or tools to use the proxy:
# Use forward proxy with curl
curl -x http://proxy.company.local:3128 https://external-api.com/data
# Set environment variables for system-wide proxy
export http_proxy=http://proxy.company.local:3128
export https_proxy=http://proxy.company.local:3128
This gave the team visibility into what external services were being called and allowed them to cache frequently accessed resources.
What You've Learned
Through Sarah's journey, you've discovered:
| Concept | Description | Key Use Case |
|---|---|---|
| Reverse Proxy | Server-side intermediary between clients and backend servers | Load balancing, SSL termination, security |
| Forward Proxy | Client-side intermediary between clients and the internet | Access control, caching, monitoring outbound traffic |
| Load Balancing Algorithms | Methods to distribute traffic (round-robin, least_conn, ip_hash) | Optimize server utilization and response times |
| Health Checks | Automatic detection of failed servers | High availability and automatic failover |
| SSL Termination | Handling HTTPS at proxy level | Simplified certificate management |
| Header Forwarding | Passing client information to backends | Logging real client IPs, maintaining request context |
Key Configuration Patterns
For Web Applications:
upstream backend {
least_conn; # Best for varying processing times
server backend1:8080;
server backend2:8080;
}
For API Services:
upstream api {
ip_hash; # Session persistence for stateful APIs
server api1:8080;
server api2:8080;
}
For Static Content:
upstream static {
# Simple round-robin works well
server static1:8080;
server static2:8080;
}
Best Practices Sarah Discovered
- Always use health checks - Let the proxy automatically detect and avoid failed servers
- Forward client information - Backend servers need X-Forwarded-For and X-Real-IP headers
- Set appropriate timeouts - Match them to your application's needs
- Enable logging - You need visibility into traffic patterns
- Use SSL termination - Simplify certificate management
- Choose the right algorithm - Consider your workload characteristics
- Plan for maintenance - Proxies allow zero-downtime deployments
Sarah's experience transformed her understanding of proxies from theoretical concept to practical tool. What started as a crisis at 2 AM became a valuable lesson in designing resilient, scalable systems.
The next time you face scaling challenges, remember Sarah's journey: sometimes the best solution isn't more servers, but smarter routing between them.
Want to try it yourself? Start with a simple Nginx reverse proxy in a test environment, send some requests through it, and watch how it distributes load. There's no better way to learn than by building it yourself.