What Is a Reverse Proxy? Explained Clearly (2026)

What Is a Reverse Proxy? Explained Clearly (2026)

A reverse proxy is a server that answers on behalf of other servers. Clients — browsers, apps, other programs — only ever talk to it. It fetches the actual response from the application behind it and hands it back. From the outside it looks like everything comes from a single machine, even when fifty services on fifty ports are running behind it.

That is the short answer. The longer one is more interesting, because almost every explanation online goes fuzzy at the same point: it explains what a reverse proxy does, but not what it differs from — and that is exactly where the decision fails in practice. Load balancer, API gateway, CDN and reverse proxy overlap so heavily that “which one do I need?” gets answered wrongly far more often than “how do I configure it?”.

So this article stays deliberately vendor-neutral. It explains the concept, separates it from its neighbours, and shows when you don’t need one. The numbers in it don’t come from other blogs but from our own servers — measured on September 14, 2026, including the measurement mistakes we made along the way.

Many clients connecting to a single public entrance with several private servers hidden behind it

What is a reverse proxy? The definition from the standard

Most explanations quote each other. It pays to look at the primary source once — and there it gets surprising: the HTTP standard doesn’t call it a “reverse proxy” at all.

RFC 9110, the current HTTP Semantics standard from June 2022, knows three kinds of intermediary: proxy, gateway and tunnel. And it says:

A “gateway” (a.k.a. “reverse proxy”) is an intermediary that acts as an origin server for the outbound connection but translates received requests and forwards them inbound to another server or servers.

A gateway presents itself to the outside as if it were the origin server, but translates the requests it receives and passes them inbound to one or more other servers. “Reverse proxy” is merely the parenthetical, the common second name.

That detail is more than pedantry, because the very next paragraph contains the decisive sentence:

All HTTP requirements applicable to an origin server also apply to the outbound communication of a gateway.

Every requirement that applies to an origin server also applies to the gateway’s outbound side. From the client’s perspective the reverse proxy is not “a thing in between” — it is the server. It may not behave like a forwarding hop; it has to stand in fully for the application behind it. Once you grasp that the thing carries a server’s full responsibility on its public side, you also understand why so many problems — wrong IPs in logs, broken redirects, cookies that never arrive — aren’t configuration mistakes but consequences of that role.

The definition explains a second thing too. The RFC explicitly names, as gateway purposes, encapsulating “legacy or untrusted information services” and enabling “partitioning or load balancing of HTTP services across multiple machines”. Hiding and distributing — those are the two core jobs; everything else is a bonus.

Forward proxy vs reverse proxy: same mechanism, opposite direction

The terms sound related, and technically they are — both forward HTTP. The difference isn’t in the technology but in who picked the intermediary.

The RFC is surprisingly clear here too. On the ordinary proxy:

A “proxy” is a message-forwarding agent that is chosen by the client, usually via local configuration rules […]

A forward proxy is chosen by the client. Your browser, your corporate network, your VPN client decides that requests go through it. The destination server often has no idea and sees only the proxy’s IP.

A reverse proxy is chosen by the server operator. The client has no say and usually no clue one exists. It types a domain, and what answers is the reverse proxy.

Forward proxyReverse proxy (gateway)
Chosen byClient / network operatorServer operator
Sits close tothe userthe application
Hidesthe client from the serverthe server from the client
Client knows about ityes, it configured itno, usually not
Typical purposeFiltering, anonymity, corporate cacheRouting, TLS, protection, one entrance
Configured inBrowser, OS, VPNServer environment

The rule of thumb that actually holds up: a forward proxy protects the client from the internet; a reverse proxy protects the internet from your servers — or your servers from the internet, depending on which you consider more at risk.

The same software can be either. Nginx, Caddy, HAProxy and Squid all run in both roles. Whether something is a forward or a reverse proxy is not a property of the program but of its position and configuration.

Two mirrored diagrams: clients funnelling outward, requests funnelling inward

How does a reverse proxy actually work?

The sequence has five steps, and step three is where most misunderstandings live.

  1. The client connects to the proxy. It resolves the domain and lands on the reverse proxy’s IP — not the application’s.
  2. The proxy terminates the TLS connection. Encryption ends here. It decrypts the request and can read it.
  3. The proxy decides, based on the request, where it goes. Usually by the Host header, often additionally by path. That’s the actual trick: the decision happens at the HTTP layer, not the IP layer.
  4. It opens its own new connection to the backend and makes the request itself.
  5. It takes the response and hands it back to the client — possibly compressed, cached, or with extra headers.

Step 3 is the reason a reverse proxy is useful at all. A server has exactly one port 443 for HTTPS. Running ten websites without a reverse proxy would need ten IP addresses or ten different ports — and nobody willingly types :8443 after a domain. The reverse proxy reads the Host header and thereby knows which of the ten sites the request was meant for.

We measured this on our own server. Three completely different applications, three different domains:

$ for h in getmind.io analytics.heynyx.dev compress.heynyx.dev; do
    curl -s -o /dev/null -w "$h -> %{http_code} remote=%{remote_ip}:%{remote_port}\n" "https://$h/"
  done
getmind.io           -> 200 remote=46.225.123.163:443
analytics.heynyx.dev -> 200 remote=46.225.123.163:443
compress.heynyx.dev  -> 200 remote=46.225.123.163:443

Same IP, same port, three different applications. That isn’t a trick, that’s the normal case — and without a reverse proxy it simply isn’t possible.

A single door branching into several separate rooms behind it

What a reverse proxy does to your request — measured, not claimed

Rather than describing what a reverse proxy changes about a request, we wrote a tiny server that simply prints what arrives. Called directly once, and once through a reverse proxy.

const http = require('http');
http.createServer((req, res) => {
  res.setHeader('content-type', 'application/json');
  res.end(JSON.stringify({ remoteAddress: req.socket.remoteAddress, headers: req.headers }, null, 1));
}).listen(3947, '127.0.0.1');

Called directly, exactly three headers arrive:

accept, host, user-agent

Through a reverse proxy (Caddy here), this arrives:

accept: */*
accept-encoding: gzip
host: 127.0.0.1:3948
user-agent: nyx-test
via: 1.1 Caddy
x-forwarded-for: 127.0.0.1
x-forwarded-host: 127.0.0.1:3948
x-forwarded-proto: http

Four headers appeared, and they’re all there for the same reason: the proxy destroyed information and is supplying a replacement. The backend now sees only the proxy as sender. Without X-Forwarded-For, no application would know who actually called it — every visitor would be 127.0.0.1.

This is precisely the problem RFC 7239 names in its abstract as the purpose of the standardised Forwarded header: it lets proxy components disclose “information lost in the proxying process”, for example the originating IP address. The RFC explicitly notes that X-Forwarded-For, X-Forwarded-By and X-Forwarded-Proto are non-standard fields; the official replacement is Forwarded. In practice the non-standard variant is the widespread one, which is worth knowing before you go hunting for a missing Forwarded line.

The trap almost everyone builds in

If the real client IP lives in a header — what stops the client from setting that header itself? Nothing. We tried it:

# Straight to the backend, proxy bypassed:
$ curl -s http://127.0.0.1:3947/ -H "X-Forwarded-For: 1.2.3.4"
  Backend sees: 1.2.3.4

The backend believes the lie immediately. So anyone who builds an application that trusts X-Forwarded-For and leaves the backend directly reachable has built a rate-limit bypass and an IP-filter bypass in one.

It gets more interesting through the proxy — and here the programs differ noticeably. We sent the same forged header through two different reverse proxies:

# Through Caddy:
$ curl -s http://127.0.0.1:3948/ -H "X-Forwarded-For: 1.2.3.4"
  Backend sees: '127.0.0.1'

# Through nginx with proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
$ curl -s http://127.0.0.1:3951/ -H "X-Forwarded-For: 1.2.3.4"
  Backend sees: '1.2.3.4, 127.0.0.1'

That’s an important and rarely mentioned difference. Caddy replaces the value. Nginx, using the most copy-pasted configuration line in the world, appends. The variable $proxy_add_x_forwarded_for does exactly what its name says: it adds the real IP to whatever the client sent.

That isn’t wrong — in a chain of several proxies it’s actually correct, because it reconstructs the full path. It becomes dangerous in combination with a second widespread piece of advice: “take the first entry from X-Forwarded-For, that’s the real client.” With this configuration the first entry is whatever the attacker wrote there. On our system, incidentally, that line lives in /etc/nginx/proxy_params — exactly the kind of file you include without ever reading it again.

The resulting rule: don’t trust the first entry. Count from the right as many entries as you have proxies of your own. Everything to the left of that is client input, and therefore unproven.

Reverse proxy vs load balancer vs API gateway vs CDN

This is where the terms separate — and where the real value of this article sits, because these four get confused constantly. The honest answer first: they aren’t four product categories but four jobs that the same kind of software can do. Every HTTP load balancer is a reverse proxy. Every API gateway is a reverse proxy. A CDN contains one.

Main jobDecides based onTypical examples
Reverse proxyGet requests to the right backendHost, pathNginx, Caddy, Apache, Traefik
Load balancerSpread load across identical serversUtilisation, availabilityHAProxy, Nginx, cloud LBs
API gatewayEnforce business rules in front of an APITokens, quotas, schemaKong, Tyk, APISIX, cloud gateways
CDNDeliver content geographically closeLocation, cache statusCloudflare, Fastly, Bunny

The distinction that actually holds:

  • A reverse proxy asks: where does this request belong? The backends are different.
  • A load balancer asks: which of these identical servers is least busy right now? The backends are interchangeable.
  • An API gateway asks: is this caller allowed to do that, and how often? It cares about the business logic, not just transport.
  • A CDN asks: can I answer this without going to the back at all? It is primarily a distributed cache.

The RFC backs this kinship explicitly: it names “partitioning or load balancing of HTTP services across multiple machines” in one breath as gateway purposes. In the standard, load balancing isn’t a separate concept but a variant of the same one.

Practically: when someone asks “reverse proxy or load balancer?”, the question is usually malformed. The right one is “are my backends different or interchangeable?” Different means you need routing. Interchangeable means you need distribution. Both means you pick a program that does both — which is essentially all of them.

A landscape with four distinct buildings connected by a shared road

TLS termination: where the encryption ends

“TLS termination” sounds technical but means something very simple: the encrypted connection ends at the reverse proxy, not at your application. The proxy decrypts, reads, decides, and often speaks unencrypted from there on.

This is the most misunderstood point, because at first it sounds like a security hole. Isn’t it unencrypted then? Yes — on the stretch between proxy and application, and that stretch usually lives on the same machine, over the loopback interface. That traffic never leaves the box.

The gain is substantial: certificates live in one place. Their renewal happens in one place. Your application needs to know nothing about TLS at all. On our server, Caddy currently manages 940 certificates fully automatically:

$ find /var/lib/caddy -path "*certificates*" -name "*.crt" | wc -l
940

Not a single one of the applications behind it contains a line of TLS code. They all speak plain HTTP on 127.0.0.1 — and that’s the whole point.

The important consequence for your application: it sees http, even though the user used https. Anyone building redirects or absolute URLs themselves will generate links to http:// — the classic redirect loop after introducing a proxy. That’s what X-Forwarded-Proto is for, and most frameworks have a “trust proxy” switch that honours it. It is almost always off by default, for good reason: without a proxy in front, it would be a security hole.

An archway with a padlock: knotted strands in front, smooth ribbons behind

Does a reverse proxy cost speed? A measurement — and a measurement error of our own

The obvious worry: surely an extra stop must be slower. We wanted to measure that rather than guess — and walked straight into our own trap first. That belongs here, because the wrong number looked more plausible than the right one.

First attempt. We called the application through the proxy using curl -k and set the host via header:

$ curl -sk -o /dev/null -w "tls=%{time_appconnect} total=%{time_total}\n" \
    https://127.0.0.1/ -H "Host: analytics.heynyx.dev"
tls=0.000000 total=0.004527

3.3 to 4.5 milliseconds — a lovely number. It was wrong. The giveaway was tls=0.000000: there is no such thing as an HTTPS connection that takes zero time for the TLS handshake. With -v it came out:

* TLSv1.3 (IN), TLS alert, internal error (592)
* OpenSSL: error:0A000438:SSL routines::tlsv1 alert internal error

The Host: header does not change SNI. curl sent 127.0.0.1 as the server name, Caddy found no certificate for it and aborted. The request returned code=000 — no response at all. And there was still a response time sitting there. A failed connection has a duration too. Had we only looked at time_total, an invented number would have made it into this article.

The lesson generalises well beyond this case: a timing measurement without a status-code check measures failures as well. And a failure is usually faster than a success — so it flatters every average it lands in.

Second attempt, correctly using --resolve, status checked, counting only 200s:

# A) Straight to the backend, no proxy
n=30 min=0.0010 median=0.0015 p90=0.0018

# B) Through the proxy, NEW TLS connection for every request
n=30 min=0.0206 median=0.0299 p90=0.0377

# C) Through the proxy, ONE connection reused
n=30 min=0.0011 median=0.0014 p90=0.0017

Those three lines are the real answer to the speed question.

Variant B looks catastrophic: 20× slower, 30 ms instead of 1.5 ms. Variant C, at 1.4 ms, is indistinguishable from direct access — its median even sits marginally below, which is simply measurement noise.

The difference between B and C is not the proxy. It’s the TLS handshake. In B every request builds a brand-new encrypted connection; in C all 30 requests share an existing one. So the actual forwarding costs something in the range of microseconds, while connection setup costs roughly 28 milliseconds.

And because real browsers reuse connections (aggressively so with HTTP/2), variant C is the realistic case and variant B the exception on very first contact. The honest answer to “does a reverse proxy cost speed?” is therefore: the proxying itself, essentially not. The TLS it handles for you, yes — but you’d have paid that anyway, just somewhere else.

When you need a reverse proxy — and when you don’t

The honest list, in both directions.

You need one when:

  • You run more than one application on a machine. From the second domain onward the question is settled.
  • You want HTTPS without implementing and renewing it inside every single application.
  • Your application listens on a port other than 80/443 — which is practically every Node, Python or Go application.
  • You run multiple instances of the same application and want to spread load.
  • You want zero-downtime deploys: the proxy switches to the new instance as soon as its health check goes green.
  • You want to enforce security headers, rate limits or access control centrally rather than maintaining them per application.

You don’t need one when:

  • You have one static website and a web server delivers it directly. A proxy in front is then a stop with no job.
  • You use a platform (Vercel, Netlify, Cloud Run, app hosting) — one is already running there, you just don’t see it. Adding your own doubles the failure modes.
  • You’re developing locally. In development the extra layer is usually just one more place where something can be misconfigured.
  • Your application already sits behind a CDN that handles routing and TLS.

The most common mistake isn’t skipping one, it’s stacking two — CDN plus your own proxy plus platform proxy — and then wondering why X-Forwarded-For has three entries and the rate limit doesn’t bite.

A reverse proxy is not a firewall replacement

This is where our own practice diverges most sharply from the theory — and we have to put it as a point against ourselves.

The usual sentence goes: “the reverse proxy hides your backends, so they’re no longer reachable.” That only holds if the backends genuinely aren’t reachable another way. We checked on our own server:

# Services listening only on 127.0.0.1:   42
# Services listening on 0.0.0.0:          22

22 services listen on all interfaces. The reverse proxy does nothing about that — anyone who knows the port can talk to it directly and bypass the proxy and all its rules. What saves us is a second, independent layer:

$ ufw status | head
Status: active
22/tcp   ALLOW  Anywhere   # SSH
80/tcp   ALLOW  Anywhere   # HTTP
443/tcp  ALLOW  Anywhere   # HTTPS

For the application ports there is no ALLOW rule, and the default policy drops them. Measured rather than assumed: of ports 3850, 5050, 3875 and 3896, not one is permitted in the firewall — even though all four listen on 0.0.0.0.

Which yields the rule that sits in our own operations handbook: bind services to 127.0.0.1, not 0.0.0.0. In Docker that means '127.0.0.1:5432:5432' instead of '5432:5432' — the default notation publishes the port to the entire world. We don’t know that from the documentation but because Germany’s federal cyber security agency wrote to us about a publicly reachable PostgreSQL instance.

A second experience from the check itself fits here: a curl to your own public IP proves nothing. The kernel routes such requests via lo and the firewall chain is never traversed — the test reports “reachable” while the port is closed from outside. A reachability test is only a test if it comes from another machine.

So the reverse proxy is a routing and visibility tool, not access control. It reduces the attack surface by reducing the number of doors anyone should be aiming at. It does not lock the other doors. The firewall does that.

The usual candidates — a map, not a recommendation

Four programs cover nearly everything. Which one fits depends less on benchmarks than on how you work.

Nginx is the default. Most widely deployed, every error message has happened to someone before, and there’s an answer for every problem. The price: its own configuration language with some sharp edges — the X-Forwarded-For business above is a good example. If you already read nginx configs, stay there; our complete nginx reverse proxy guide covers exactly that. And if you’re still deciding which web server at all, Apache vs Nginx answers it.

Caddy obtains certificates automatically, with no extra tooling. That’s what the 940 certificates above are — we didn’t have to set anything up for them. Its configuration is far shorter; a working reverse proxy with HTTPS is two lines. The price: a smaller community, so exotic cases are harder to look up.

Traefik discovers services by itself. In Docker or Kubernetes environments it reads labels and configures itself from them. When containers come and go constantly, that’s the right approach. On a classic server it’s effort without payoff.

HAProxy is the distribution specialist. When load balancing is the main job rather than a by-product, it’s the most precise choice — with the best tooling for health checks and load distribution.

Then there are the hosted ones: Cloudflare, Fastly and others are reverse proxies somebody else operates. They take the work off your hands and the control along with it. For many cases a good trade — you should just know you’re making it. If you’re weighing hosted platforms generally, our Vercel alternatives comparison is the right entry point.

Our own setup uses Caddy, but that’s a preference, not a recommendation: we run many small services across many domains, and automatic certificate management removes exactly the part that otherwise creates work. For a single large application the advantage would be small.

Common mistakes with your first reverse proxy

The problems practically everyone hits once — and how to recognise them.

502 Bad Gateway. The proxy can’t reach the backend. Almost always: the application isn’t running, listens on a different port, or listens on 127.0.0.1 while the proxy sits in a container and therefore arrives from outside. The first test is always to call the backend directly — if it answers there, it’s the proxy; if it doesn’t, it’s the application.

504 Gateway Timeout. The backend answers, but too slowly. A default 60-second timeout is quickly reached by a file upload or a long report. Note: raising the timeout is rarely the right fix, it only moves the boundary.

Endless redirect loop. The application sees http, redirects to https, the proxy forwards the request as http again. That’s the X-Forwarded-Proto case from above — turn on “trust proxy”.

All visitors share one IP. The application reads the socket address instead of X-Forwarded-For. Often only noticed when a rate limit suddenly hits everyone at once.

WebSockets break. WebSocket connections need a protocol upgrade that not every proxy passes through by itself. Nginx needs the Upgrade and Connection headers for it; Caddy and Traefik handle it automatically.

Uploads fail beyond a certain size. The proxy has its own request body size limit, often 1 MB. The application never sees the request and therefore can’t produce a sensible error either.

What all six share: the application is fine, and the error appears somewhere you aren’t looking. Which is why, for any proxy problem, testing direct backend access first pays off. That one question — “does it work without the proxy?” — halves the search time.

A balance scale weighing a feather against a shield

Frequently asked questions

What is a reverse proxy in simple terms?

A server that answers on behalf of other servers. Clients only talk to it, it fetches the response from the actual application behind it and hands it back. From outside it looks like a single machine even when many services run behind it. In the HTTP standard RFC 9110 this component is actually called a “gateway”; “reverse proxy” is only the common second name there.

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

Who chose it. A forward proxy is chosen by the client — RFC 9110 describes it as an intermediary “chosen by the client”, typically via browser or network settings. A reverse proxy is chosen by the server operator, and the client usually notices nothing. The forward proxy hides the client from the server; the reverse proxy hides the servers from the client. Technically it is the same kind of software pointed in the opposite direction.

Is a reverse proxy the same as a load balancer?

Not quite, but they overlap heavily. Every HTTP load balancer is a reverse proxy; not every reverse proxy is a load balancer. The difference is in the backends: a reverse proxy routes to different applications and decides by host and path. A load balancer distributes across interchangeable instances of the same application and decides by utilisation and availability.

Does a reverse proxy make my website slower?

The forwarding itself costs almost nothing. In our measurement, direct access had a median of 1.5 ms and access through the proxy over a reused connection 1.4 ms — indistinguishable. Only the case where a new TLS connection was built for every single request was substantially more expensive: 29.9 ms. But that cost comes from the TLS handshake, not the proxying, and real browsers avoid it by reusing connections.

Do I need a reverse proxy for a single website?

Usually not, if it is a static site delivered directly by a web server. But as soon as the site comes from your own application on its own port — Node, Python, Go — you need something to occupy port 443 and handle HTTPS. From the second domain on the same machine onward the question is settled, because there is only one port 443.

Does a reverse proxy replace a firewall?

No. It reduces the number of doors anyone should aim at, but it does not lock the rest. On our own server 22 services listen on all interfaces; if the firewall did not drop those ports, they would be directly reachable despite the reverse proxy. Services belong bound to 127.0.0.1, and the firewall remains a separate, independent layer.

What is TLS termination?

The encrypted connection ends at the reverse proxy instead of at the application. The proxy decrypts the request, decides on a destination, and usually speaks unencrypted from there — typically over the loopback interface of the same machine, so that traffic never leaves the box. The advantage is that certificates only have to live and be renewed in one place.

Why does my application behind the proxy only see one IP address?

Because the proxy opens its own new connection to the backend. From the application’s point of view the proxy is the sender. It passes the real address along in X-Forwarded-For — RFC 7239 describes exactly that as the purpose: disclosing information lost in the proxying process. The important part is to trust that header only as far as your own proxies in the chain, since clients can set it themselves.

Conclusion: a component you only miss once it’s gone

A reverse proxy is not an optimisation tool but a structural one. It solves a problem that follows from a plain fact: there is only one port 443, but usually more than one application. Everything else — TLS in one place, load distribution, central headers, zero-downtime deploys — comes on top, because somebody is already standing there seeing every request.

What we learned writing this article sits in the measurements, not the definitions. That the surcharge for proxying lives in microseconds and the apparent 20× factor is really the TLS handshake. That two widespread reverse proxies react differently to a forged X-Forwarded-For, and the most copy-pasted nginx line leaves the client’s input in place. And that our own server runs 22 services on all interfaces that the reverse proxy protects not at all — the firewall does.

The most honest moment was the first measurement attempt, which produced a clean number even though no connection was ever established. A timing measurement without a status check measures failures too — and failures are fast. Had we not looked closer, an invented but entirely plausible-looking number would be sitting here.

If you want to get concrete now, the next step is configuration, and for that there’s our detailed nginx reverse proxy guide with step-by-step setup, SSL, caching and hardening. If the server isn’t up yet, start with setting up a Linux server — it also covers the firewall rules that have to stand next to the proxy. And if you’re curious what happens when those layers are missing: we once didn’t notice for five days that our server had been hijacked.