The most surprising thing about reverse proxies is that they don’t actually do anything interesting themselves; their entire purpose is to get out of the way and make other things look better.

Let’s see what that looks like. Imagine you have a fleet of identical web servers, each running the same application. A user requests www.example.com/status. Without a reverse proxy, you’d have to pick one server, say web1.internal, to handle that request. But what if web1.internal is overloaded? Or worse, what if it goes down?

Here’s a simplified Nginx configuration that acts as a reverse proxy for two backend web servers, web1.internal and web2.internal.

http {
    upstream backend_servers {
        server web1.internal:80;
        server web2.internal:80;
    }

    server {
        listen 80;
        server_name www.example.com;

        location / {
            proxy_pass http://backend_servers;
            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;
        }
    }
}

When a request hits Nginx on port 80 for www.example.com, the location / block tells Nginx to forward it to the backend_servers upstream group. Nginx, using its default round-robin algorithm, will send the request to either web1.internal:80 or web2.internal:80. The proxy_set_header directives are crucial; they inject information about the original request (like the client’s IP address and the requested hostname) into the request that Nginx sends to the backend server, so the backend application knows who the real client is and what they asked for.

The problem this solves is making your application resilient and scalable. Instead of users hitting individual servers, they hit the reverse proxy. The proxy can then distribute traffic across multiple backend servers. If one backend server fails, the proxy stops sending traffic to it, and the application remains available. You can also add more backend servers behind the proxy as traffic grows, without changing the public-facing address.

Internally, Nginx (or HAProxy, or Traefik) is essentially a highly optimized network listener and request forwarder. It accepts incoming connections, inspects the request (HTTP method, URL, headers), decides where to send it based on its configuration, establishes a new connection to the appropriate backend server, copies the request data, and then streams the backend’s response back to the original client. It’s a middleman that’s really good at its job.

The key levers you control are:

  • Upstream groups: Defining pools of backend servers.
  • Load balancing algorithms: How traffic is distributed (round-robin, least connections, IP hash).
  • Health checks: How the proxy determines if a backend server is healthy and should receive traffic.
  • SSL termination: Decrypting HTTPS traffic at the proxy so backend servers don’t have to.
  • Request routing: Directing traffic to different upstream groups based on URL paths, hostnames, or headers.
  • Response manipulation: Modifying headers or content before sending to the client.

Many people configure reverse proxies to simply forward traffic. But when you enable SSL termination on the proxy, Nginx decrypts the incoming HTTPS connection. It then sends the request to the backend servers over plain HTTP (if they’re on a trusted internal network). This offloads the CPU-intensive SSL/TLS encryption/decryption work from your application servers, allowing them to focus solely on serving application logic, and simplifies certificate management as you only need to manage certificates on the proxy.

The next step is often understanding how to implement advanced routing rules based on request headers or cookies.

Want structured learning?

Take the full Computer Networking course →