Skip to the solution
EasySystem Design10 min read

What is the difference between a reverse proxy and a forward proxy?

900 views

The problem

Question presented to candidate: "People use the word 'proxy' for two quite different things. Explain the difference between a forward proxy and a reverse proxy β€” who sets each one up, who it protects, and what you would use each for. Then tell me how a backend behind a reverse proxy finds out the real client IP."

What a strong answer should cover:

  • The core distinction: a forward proxy acts on behalf of clients (the server sees the proxy, not the user); a reverse proxy acts on behalf of servers (the client sees the proxy, not the backends)
  • Who configures it: clients are configured to use a forward proxy (browser settings, HTTPS_PROXY, PAC files); a reverse proxy is invisible to clients, who just resolve the site name to it
  • Uses: forward β€” egress filtering, auditing, shared caching, a fixed egress IP; reverse β€” load balancing, TLS termination, caching, compression, routing, hiding and protecting backends
  • HTTPS through a forward proxy uses the CONNECT method to tunnel, so the proxy sees the host name but not the content unless it intercepts TLS
  • Client identity behind a reverse proxy: X-Forwarded-For / RFC 7239 Forwarded, why the left-most entry is spoofable, and trusting only known proxy hops

Code / implementation expected: No β€” a clear contrast with examples is enough; the example shows who sees which address and how to parse X-Forwarded-For safely.

  • networking
  • proxy

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

10 min read Β· 9 sections

Read the code

Forward vs reverse proxy: who sees which IP, and reading X-Forwarded-For safely
// Forward proxy vs reverse proxy, as packets passing through in-process hops.
// Each hop sees only the TCP peer address (src) and the headers it was given.
// No real network: a "request" is an object, a "hop" is a function.

function forwardProxy(req, policy) {
  // Acts for the CLIENTS: knows every user, enforces their company policy.
  if (policy.blocked.includes(req.host)) return { status: 403, seenBy: "proxy", note: "blocked by company policy" };
  const outbound = { ...req, src: "198.51.100.7" }; // the proxy opens its own connection
  return origin(outbound);
}

function origin(req) {
  return { status: 200, seenBy: req.host, note: `origin sees client ${req.src}` };
}

console.log("Part 1: forward proxy (in front of the clients)");
const policy = { blocked: ["games.example"] };
for (const [user, host] of [["10.0.0.5", "news.example"], ["10.0.0.6", "news.example"], ["10.0.0.6", "games.example"]]) {
  const r = forwardProxy({ src: user, host, headers: {} }, policy);
  console.log(`  ${user} -> ${host.padEnd(13)} ${r.status}  ${r.note}`);
}

// Reverse proxy: acts for the SERVERS. Clients only know its public address.
const BACKENDS = { "/api/": ["app-1", "app-2"], "/static/": ["files-1"] };
let rr = 0;
function reverseProxy(req, proxyIp) {
  const prefix = Object.keys(BACKENDS).find((p) => req.path.startsWith(p));
  if (!prefix) return { status: 404, backend: "-", xff: "-" };
  const pool = BACKENDS[prefix];
  const backend = pool[rr++ % pool.length];
  const prior = req.headers["x-forwarded-for"];
  const xff = prior ? `${prior}, ${req.src}` : req.src; // append the peer it saw
  return { status: 200, backend, src: proxyIp, xff };
}

console.log("\nPart 2: reverse proxy (in front of the servers)");
for (const path of ["/api/orders", "/api/users", "/static/logo.png", "/admin"]) {
  const r = reverseProxy({ src: "203.0.113.9", path, headers: {} }, "10.1.0.2");
  console.log(`  GET ${path.padEnd(17)} ${r.status} -> ${r.backend.padEnd(7)} X-Forwarded-For: ${r.xff}`);
}

// Who is the real client? Walk X-Forwarded-For from the right, skipping hops we trust.
const TRUSTED = ["10.1.0.2", "172.16.0.4"]; // our CDN edge and our reverse proxy
function leftmost(xff) { return xff.split(",")[0].trim(); }
function rightmostUntrusted(xff, peer) {
  const hops = [...xff.split(",").map((s) => s.trim()), peer];
  for (let i = hops.length - 1; i >= 0; i--) if (!TRUSTED.includes(hops[i])) return hops[i];
  return hops[0];
}

console.log("\nPart 3: reading the client IP behind two proxies (edge 172.16.0.4, then 10.1.0.2)");
const cases = [
  ["honest client", "203.0.113.9", ""],
  ["spoofing client", "192.0.2.66", "1.2.3.4"],
];
for (const [name, clientIp, forged] of cases) {
  let headers = forged ? { "x-forwarded-for": forged } : {};
  const atEdge = reverseProxy({ src: clientIp, path: "/api/login", headers }, "172.16.0.4");
  headers = { "x-forwarded-for": atEdge.xff };
  const atProxy = reverseProxy({ src: "172.16.0.4", path: "/api/login", headers }, "10.1.0.2");
  console.log(`  ${name}: header at app = "${atProxy.xff}", TCP peer = ${atProxy.src}`);
  console.log(`    leftmost entry:         ${leftmost(atProxy.xff)}`);
  console.log(`    rightmost untrusted:    ${rightmostUntrusted(atProxy.xff, atProxy.src)}`);
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 2 of 106 in System design

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextEasy

What latency numbers and availability targets should every engineer know?

Continue