# What is a reverse proxy? Caddy, nginx and Cloudflare explained

> A reverse proxy is a server that sits in front of your website or app: it receives every request from the internet and forwards each one to the application behind it. What it is used for, the minimal Caddy and nginx configs, and the headers that keep it from breaking your site.

Updated 2026-10-09 · Best practices · HTML version: https://getreport.app/guides/what-is-a-reverse-proxy

A reverse proxy is a server that sits in front of your website or app: it receives every request from the internet, forwards each one to the right application behind it, and relays the answer back. Visitors only ever talk to the proxy; your app never faces the internet directly. Almost every production site has one (nginx or Caddy on the server, a load balancer at the host, or Cloudflare in front of everything) and it is where [TTFB, the time your server takes to send the first byte](https://getreport.app/guides/ttfb-what-a-slow-server-looks-like), is often won or lost: a proxy can answer from cache in milliseconds, or add a wasted hop and a redirect loop. This guide explains what a reverse proxy does, how it differs from a forward proxy, the minimal Caddy and nginx configs, what Cloudflare adds, and the three ways a proxy breaks a site when its headers are wrong.

## Quick answer

- A reverse proxy receives every request for your site on ports 80 and 443 and passes it to your app over the internal network. The visitor sees one server; behind it there can be many.
- The usual jobs: terminating TLS, caching responses, compressing them, balancing load across several app servers, serving many apps from one IP address, and keeping the origin's address private.
- In Caddy, two lines (`example.com` and `reverse_proxy localhost:3000`) give you a working reverse proxy with HTTPS included.
- In nginx, it is a `proxy_pass` line plus `proxy_set_header` lines for `Host`, `X-Forwarded-For` and `X-Forwarded-Proto`: without them the app sees the wrong hostname, the proxy's IP and the wrong scheme.
- Cloudflare is a reverse proxy you rent: a proxied DNS record puts its network between visitors and your server.
- The costs: one extra hop on every uncached request, redirect loops when the app does not know the visitor used HTTPS, and logs full of the proxy's IP instead of visitors' addresses.

## What a reverse proxy does

When a visitor opens your site, their browser connects to the reverse proxy, because that is where the domain's DNS record points. The proxy accepts the connection, usually handles the TLS handshake, and reads the request. Then it opens its own connection to your application (a Node process on port 3000, PHP-FPM, a container) forwards the request, waits for the answer, and sends it back to the visitor. The application listens only on localhost or a private network, never on a public port.

![Three browsers connect over HTTPS to one reverse proxy, which terminates TLS and keeps a cache, then forwards requests over a private network to two application servers](https://getreport.app/guides/img/what-is-a-reverse-proxy/diagram.svg "Visitors only ever reach the proxy; the apps behind it listen on private ports and can be swapped, scaled or restarted without anyone noticing.")

That position in the middle is the whole point. Because every request passes through one place, that place can do work the app would otherwise do itself: encrypt, cache, compress, route, limit. And because the apps behind it are invisible from outside, you can run three on one machine, restart one, or move one to another server by changing a line in the proxy's config.

## Reverse proxy vs forward proxy

The two share a name and a mechanism (both relay HTTP for someone else) but they face opposite directions:

| | Forward proxy | Reverse proxy |
| --- | --- | --- |
| Sits in front of | Clients (browsers, a whole office) | Servers (your app) |
| Works on behalf of | The person requesting | The site answering |
| The other side sees | The proxy, not the real visitor | The proxy, not the real server |
| Typical use | Company network filtering, privacy, geo-unblocking | TLS, caching, load balancing, routing |
| Who sets it up | The visitor or their network admin | The site owner |

A forward proxy hides who is asking: the website sees the proxy's address, not the employee's laptop. A reverse proxy hides what is answering: the visitor sees one address, not the pool of app servers behind it. When someone says "proxy" with no qualifier, they almost always mean the forward kind; everything in the rest of this guide is the reverse kind.

## What a reverse proxy server is used for

Six jobs cover almost every deployment:

- **TLS termination.** The proxy holds the certificate and does the HTTPS handshake; behind it, traffic can travel as plain HTTP over localhost or a private network. One place to get certificates, renew them and keep protocol settings current, no matter how many apps sit behind it.
- **Caching.** The proxy stores responses and answers repeat requests itself, so a page that took the app 600 ms to build goes out in a few milliseconds without the app being asked. This is the single biggest TTFB lever on dynamic sites.
- **Compression.** The proxy compresses text responses with gzip, Brotli or zstd on the way out, so each app does not need its own compression setup.
- **Load balancing.** With several app servers, the proxy spreads requests across them and stops sending to one that fails its health check. The visitor-facing address never changes.
- **One IP, many apps.** The proxy reads the hostname of each request (`app.example.com` vs `example.com`) and routes to a different backend. This is how one small VPS serves a site, an API and an admin panel on a single public address.
- **Hiding the origin.** Only the proxy's address is public. Your app server's real IP stays out of DNS, which keeps direct attacks and stray crawlers away from it: as long as nothing else leaks the address.

## A minimal reverse proxy config: Caddy and nginx

In Caddy, a complete reverse proxy for one app is the whole config file:

```caddy
example.com
reverse_proxy localhost:3000
```

Caddy fetches and renews the certificate, redirects HTTP to HTTPS, and sets the forwarding headers (`X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`) by default, while passing the `Host` header through unchanged: which is exactly what most apps expect. What the two lines switch on, and the security headers and cache lifetimes you still add yourself, is covered in [Caddy: the two-line HTTPS server](https://getreport.app/guides/caddy-the-two-line-https-server).

In nginx, the same thing is a `location` block, and the header lines are not optional:

```nginx
server {
    listen 443 ssl;
    http2 on;
    server_name example.com;
    # certificate, TLS and security header config omitted

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
```

Each `proxy_set_header` line repairs something the proxying breaks:

- **`Host $host`** passes the hostname the visitor typed. Without it, nginx sends the backend's own name (`127.0.0.1:3000`), and the app builds links, redirects and cookies for the wrong host. A CMS behind a proxy with this line missing redirects visitors to `localhost`.
- **`X-Forwarded-For`** appends the visitor's IP address to the request, because at TCP level the app only ever sees the proxy connecting. Without it, every visitor in your logs is `127.0.0.1`, and rate limiting, geo features and abuse blocking in the app all act on one shared address.
- **`X-Forwarded-Proto $scheme`** tells the app the visitor used `https`, even though the proxy-to-app leg is plain `http`. Apps use it to decide whether to redirect to HTTPS and whether to mark cookies `Secure`.

The TLS settings, security headers and cache lifetimes that belong in the same server block (and the `add_header` inheritance trap that silently drops them) are in [Nginx configuration for an A grade](https://getreport.app/guides/nginx-configuration-for-an-a-grade).

One warning for the receiving side: an app must only trust these headers when they come from its own proxy, because anyone can send a request with a forged `X-Forwarded-For`. Frameworks have a switch for this (Express calls it `trust proxy`, Django `SECURE_PROXY_SSL_HEADER`); set it to trust exactly the proxy's address, not everything.

## Cloudflare: a reverse proxy you rent

You do not have to run the proxy yourself. When a DNS record in Cloudflare is set to Proxied (the orange cloud), the name resolves to Cloudflare's network instead of your server: Cloudflare terminates TLS near the visitor, applies its caching and firewall rules, and forwards the request to your origin. That is a reverse proxy in every sense; you just rent it, and its front end happens to be in hundreds of cities instead of one.

The same header rules apply, with Cloudflare's own names: the visitor's address arrives in `CF-Connecting-IP` (and `X-Forwarded-For`), and your server should restore it in its logs only for requests that really come from Cloudflare's published IP ranges. And because Cloudflare-to-origin is its own connection, keep it encrypted: SSL/TLS mode Full (strict), never Flexible; Flexible sends plain HTTP to an origin that redirects to HTTPS, which is the classic "too many redirects" loop.

Renting the front does not retire your own proxy. A typical small production stack is both: Cloudflare at the edge, Caddy or nginx on the server terminating the origin connection and routing to the apps. getreport.app itself runs exactly that.

## What a reverse proxy costs you

Nothing in the middle is free. Three costs come up again and again:

**An extra hop on every uncached request.** The proxy receives, inspects and re-sends each request. On the same machine that overhead is well under 1 ms; to a proxy in the same data centre, a few milliseconds. The hop that shows up in reports is the rented kind: when an edge proxy has no cached copy, the request travels visitor → edge → origin, and a far-away edge city adds that detour to every uncached page. Caching pays the toll back; a proxy that answers most requests from cache makes the site faster, not slower.

**Redirect loops when the app does not know about the proxy.** The app receives plain HTTP from the proxy, concludes the visitor is insecure, and redirects to `https://`; which arrives at the app as plain HTTP again, forever. The browser gives up with ERR_TOO_MANY_REDIRECTS. The fix is the `X-Forwarded-Proto` header plus an app configured to trust it; the full diagnosis is in [fixing redirect loops](https://getreport.app/guides/err-too-many-redirects).

**Visitors' real IP addresses disappear.** Every connection the app sees comes from the proxy, so without `X-Forwarded-For` (or `CF-Connecting-IP`), logs, rate limits and "login from a new device" checks all see one address. Symptoms are quiet: analytics that claim every visitor is in your data centre's city, or a rate limiter that blocks everyone at once because they share the proxy's IP.

## Is a CDN a reverse proxy?

Yes. A CDN is a reverse proxy with many front doors: the same receive-and-forward machinery, duplicated in cities around the world, with caching as its main job. The differences are degree, not kind; a single nginx box proxies for one origin from one place; a CDN does it from the edge location nearest each visitor, which is what removes distance from the first byte. What that distance is worth, and what edge caching can and cannot fix, is the subject of [What is a CDN? What it fixes and what it does not](https://getreport.app/guides/cdn-basics-what-it-fixes-and-what-it-does-not).

The practical consequence: if you are behind a CDN, you are already behind a reverse proxy, and every header rule above applies. Most redirect-loop and wrong-IP problems blamed on a CDN are ordinary reverse proxy header problems.

## How to tell if a site uses one

The response headers usually say. Run `curl -sI https://example.com/` and look at the `server` line: `cloudflare` means Cloudflare's proxy answered, `nginx` or `Caddy` names the software that sent the response; which, behind a proxy, is the proxy, not the app. A `cf-ray`, `x-served-by` or `x-cache` header is a proxy or CDN announcing itself, and some add a `Via` header, which the HTTP standard (RFC 9110) asks intermediaries to send, though many stay silent.

> **Free tool:** [CMS detector: free website technology checker](https://getreport.app/tools/tech-stack): Free CMS detector: see what any website is built with, from CMS and framework to JavaScript libraries, CDN and server, plus known vulnerabilities per version.

The detector loads the page in Chromium and reads its HTML, headers, cookies and script URLs, then names what they reveal: the web server or proxy from the `server` header, a CDN from the fingerprints it leaves (such as Cloudflare's `cf-ray`), alongside the CMS and libraries. A proxy that strips its own headers stays invisible to it; it only names what the page reveals.

To see what the proxy does to response time, measure the first byte:

> **Free tool:** [TTFB test: check your time to first byte](https://getreport.app/tools/ttfb-test): Free TTFB test: measure time to first byte from Frankfurt, in a Lighthouse run and from real Chrome users, plus the redirects before the page. No sign-up.

A proxied, cached page answers in tens of milliseconds; a proxied, uncached one carries the full trip to the origin plus the app's build time. Run the test twice: a second run that is much faster than the first is the proxy's cache warming up.

> **Check: Time to First Byte.** TTFB is how long the server takes to start answering. Everything else waits for it, so a slow first byte makes every other metric worse. Google's 800 ms target is for real visitors, network included; Lighthouse flags the server's own share above 600 ms (see server response time).
>
> 1. Add page caching (WordPress: WP Rocket, LiteSpeed Cache, or your host's cache; Shopify does this for you).
> 2. Put a CDN in front (Cloudflare, Bunny) so the first byte comes from a nearby edge.
> 3. Check database-heavy plugins and slow hosting; a plan with more CPU often fixes this outright.

> **Check: Server response time.** Lighthouse flags a first byte slower than 600 ms. Caching and hosting fix this for most CMS sites.
>
> 1. Enable full-page caching (WordPress: WP Rocket, LiteSpeed Cache, WP Super Cache; or your host's server cache).
> 2. Use a CDN and keep database queries out of the request path.

## Questions people ask

### What is a reverse proxy in simple terms?

It is a receptionist for your server. Visitors never walk into the back office: every request arrives at the front desk, which checks it, passes it to the right person inside, and brings the answer back. The visitor only ever deals with the desk, so the people behind it can move, multiply or take a break without anyone outside noticing. In technical terms: one public server that receives all traffic and forwards it to private applications behind it.

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

Direction. A plain (forward) proxy sits in front of the person browsing and acts for them: the website sees the proxy, not the visitor, which is how office networks filter traffic. A reverse proxy sits in front of the website and acts for it: the visitor sees the proxy, not the real server. Same relay mechanism, opposite owner; the visitor sets up a forward proxy, the site owner a reverse one.

### Is Cloudflare a reverse proxy?

Yes: that is precisely what the orange-cloud "Proxied" setting on a DNS record means. The domain resolves to Cloudflare's network, Cloudflare terminates the visitor's TLS connection, applies caching and firewall rules, and forwards the request to your server. With the setting grey ("DNS only"), Cloudflare just answers DNS queries and visitors connect straight to your server, so none of the proxying, caching or protection applies.

### Is a CDN the same as a reverse proxy?

A CDN is one kind of reverse proxy: the same sit-in-front-and-forward design, repeated in many locations, with caching as the main job. Every CDN is a reverse proxy, but not every reverse proxy is a CDN; nginx on your own server proxies from one place and ships with no edge network. The header behaviour is identical, which is why CDN redirect loops and wrong-IP logs have the same fixes as any proxy.

### Do I need a reverse proxy?

If you run your own server, almost certainly yes. It is the standard way to put HTTPS in front of an app that speaks plain HTTP, serve more than one app from one machine, and add caching and compression in one place: Caddy does it in two config lines. If you are on Shopify, Wix, or managed hosting, or behind Cloudflare, you already have one; it is part of the platform rather than something to install.

### Does a reverse proxy slow down a website?

Measurably, almost never, and usually the opposite. A proxy on the same machine adds well under 1 ms per request, which no visitor can perceive. In return it serves cached responses in milliseconds instead of the hundreds of milliseconds a dynamic page costs, and compresses what it sends. The real slowdowns blamed on proxies are misconfigurations: no caching enabled, a far-away edge with an uncached origin, or a redirect loop from a missing X-Forwarded-Proto header.
