A reverse proxy is a server that sits in front of your application, receives requests from clients and forwards them to the app, then sends the app's response back. Clients only ever talk to the proxy. It's where TLS is terminated, requests are routed to the right backend, and traffic is load-balanced, cached, compressed or rate-limited. nginx, Caddy, HAProxy, Traefik, Envoy and cloud load balancers are all reverse proxies.
server {
listen 443 ssl;
server_name api.example.com;
location /webhooks/ {
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_read_timeout 30s;
client_max_body_size 10m;
}
}
How a reverse proxy works
- A client resolves
api.example.comto the proxy's IP and opens an HTTPS connection to it. - The proxy terminates TLS, reads the request and picks an upstream based on host, path or load-balancing rules.
- It opens a connection to the upstream, for example
127.0.0.1:3000, and forwards the request, adding headers such asX-Forwarded-Forso the app knows the original client. - It relays the upstream's response to the client, or generates an error if the upstream fails.
Forward proxy vs reverse proxy
| Forward proxy | Reverse proxy | |
|---|---|---|
| Acts on behalf of | Clients | Servers |
| Sits in front of | Users' devices | Your application |
| Typical use | Filtering, privacy, egress control | TLS, routing, load balancing, caching |
| Who configures it | The client side | The server side |
Errors a reverse proxy generates
When something goes wrong behind the proxy, the error comes from the proxy, not your app:
- 502 Bad Gateway: the proxy couldn't get a valid response. The app is down, listening on another port, bound to the wrong interface, or crashed mid-request.
- 504 Gateway Timeout: the app accepted the connection but didn't answer within the proxy's timeout (
proxy_read_timeoutdefaults to 60 seconds in nginx). - 503 Service Unavailable: no healthy upstreams, or the proxy is shedding load.
- 413 Payload Too Large: the body is over
client_max_body_size(1 MB by default in nginx). - 429 Too Many Requests: a rate limit such as nginx
limit_reqfired.
Reverse proxies and webhooks
Most webhook endpoints sit behind a reverse proxy, and a few settings matter:
- Timeouts. Providers time out in seconds. If your proxy waits 60 seconds for a slow handler, the provider has already given up and will retry. Keep handlers fast.
- Body size. Large payloads, such as GitHub push events with many commits, can exceed the default 1 MB body limit.
- Don't modify the body. Compression, buffering rewrites or body-inspecting middleware that changes bytes will break signature verification.
- Preserve headers. Signature headers like
Stripe-Signatureorwebhook-signaturemust reach the app. Some proxies drop headers with underscores (nginx does unlessunderscores_in_headers on). - The proxy doesn't store requests. If the app is down, the provider gets a 502 and you depend entirely on its retry policy. A webhook gateway is the store-and-retry alternative.
Reverse proxies for localhost and private networks
A classic reverse proxy needs a public IP and an open port. When the backend is on a laptop or behind a firewall, a tunnel does the same job from the outside: a hosted reverse proxy on the internet, connected to your machine by an outbound connection the agent opens. With Webhook Relay, relay connect --name my-app http://localhost:3000 gives your local server a public HTTPS URL, and the Host header can be rewritten for apps that check it. For webhooks, webhook forwarding adds storage, durable retries and fan-out on top.
