Reverse Proxy Concepts #
The ability to act as a reliable, secure, and efficient gateway for web traffic is a main pillar of modern application infrastructure. This is where the reverse proxy plays a crucial role by separating public clients from the backend servers running the application logic. Caddy provides a highly sophisticated yet intuitively configured reverse proxy implementation, automating complex tasks like TLS termination, connection pool management, and server health monitoring transparently without burdening your application code logic.
Forward Proxy vs Reverse Proxy #
To understand the use of a reverse proxy, we need to clarify the fundamental difference between a forward proxy and a reverse proxy. The main difference lies in whom the proxy represents and the communication vantage point within the network.
+-----------------------------------------------------------------------------+
| 1. FORWARD PROXY (Represents the Client): |
| Internal Client ===> [ Forward Proxy ] ===> Internet ===> Target Server |
| (The client knows the proxy address; the target server only sees the proxy IP) |
| |
| 2. REVERSE PROXY (Represents the Server): |
| Public Client ========> [ Reverse Proxy ] ========> Internal Backend Server |
| (The client thinks the proxy is the target server; the backend is isolated) |
+-----------------------------------------------------------------------------+
1. Forward Proxy (Represents the Client) #
A forward proxy acts on behalf of the client. The client is explicitly configured to send all requests to the proxy before they’re forwarded to the public internet.
- Main Purpose: Protecting client privacy, bypassing geographic blocks or censorship, and caching content to save local network bandwidth.
- Security: Target servers on the outside internet don’t know the client’s real IP address; they only see connections coming from the forward proxy’s IP.
- Usage Examples: Corporate internal networks restricting employee access to certain websites, VPN (Virtual Private Network) services, or the Tor network.
2. Reverse Proxy (Represents the Server) #
A reverse proxy acts on behalf of one or more backend servers. Public clients on the outside internet are unaware of this proxy’s existence; they send requests normally to the reverse proxy’s domain name or IP address, thinking they’re communicating directly with the server running the application.
- Main Purpose: Protecting backends from direct internet exposure, distributing traffic loads (load balancing), handling SSL/TLS encryption centrally (TLS termination), and performing response compression and caching.
- Security: Your backend servers are hidden inside an isolated private network. Outside attackers can’t scan or attack the backend servers directly because all interaction is filtered through the reverse proxy.
- Usage Examples: Caddy placed in front of Node.js, Django, Laravel, or Go applications running on internal ports (like
:3000,:8000, or via Unix Socket).
Here’s a visualization of the communication flow to clarify the difference:
flowchart TD
subgraph Internal_Client["Client Network (Private)"]
Client1["Client A"]
Client2["Client B"]
end
subgraph Proxy_Forward["Forward Proxy"]
FP["Forward Proxy Server"]
end
Internet1["Public Internet"]
subgraph Target_Servers["External Target Servers"]
SrvA["Website X"]
SrvB["Website Y"]
end
Client1 --> FP
Client2 --> FP
FP -->|"Represents Clients A & B"| Internet1
Internet1 --> SrvA
Internet1 --> SrvB
style FP stroke:#0288d1,stroke-width:2px
style SrvA stroke:#43a047,stroke-width:2px
style SrvB stroke:#43a047,stroke-width:2pxConversely, in a reverse proxy architecture, outside clients enter through the internet toward one main gateway that distributes requests to various services on the internal network:
flowchart TD
subgraph External_Clients["Public Clients (Internet)"]
User1["Desktop User"]
User2["Mobile App"]
end
subgraph Main_Gateway["Main Gateway"]
RP["Caddy Reverse Proxy"]
end
subgraph Backend_Network["Backend Network (Private)"]
direction LR
App1["App Server 1 (NodeJS :3000)"]
App2["App Server 2 (Go :8080)"]
DB["Database Server"]
end
User1 --> RP
User2 --> RP
RP -->|"Forward to App 1"| App1
RP -->|"Forward to App 2"| App2
App1 -. Data Access .-> DB
App2 -. Data Access .-> DB
style RP stroke:#0288d1,stroke-width:2px
style App1 stroke:#43a047,stroke-width:2px
style App2 stroke:#43a047,stroke-width:2px
style DB stroke:#e53935,stroke-width:2pxWhy Do We Need a Reverse Proxy? #
Running a web application directly facing the public internet without an intermediary layer is very risky and inefficient. Here are the reasons why using a reverse proxy like Caddy has become the industry standard:
1. TLS Termination #
Handling SSL/TLS encryption and decryption requires significant CPU computing power. If every backend application server must do TLS handshakes and decrypt every data packet independently, application performance drops drastically.
flowchart TD
Client["Client"] -->|"HTTPS (Encrypted)"| Caddy["Caddy Reverse Proxy"]
Caddy -->|"TLS Terminated / Connection Forwarded Locally"| NodeJS["NodeJS App :3000"]With Caddy at the front line:
- Caddy handles the heavy TLS handshakes at the outer network level.
- Caddy decrypts HTTPS traffic and forwards plain HTTP requests (unencrypted) to the backend application servers over a secure internal network (like
127.0.0.1or a private VPC). - You don’t need to configure SSL/TLS certificates inside your application code (Node.js, Go, Python, etc.). The application code stays simple and focused on business logic.
- The SSL/TLS certificate renewal process is fully automated by Caddy centrally.
2. Load Balancing #
For large-scale applications, a single backend server instance can’t handle user traffic spikes. You must run several identical application instances on different physical servers or ports.
The reverse proxy acts as a traffic cop that receives all incoming requests and distributes them evenly across the available backend instances. If one instance crashes or is under maintenance, Caddy detects the failure and automatically shifts traffic to another healthy backend without causing downtime for users.
3. Path-Based and Host-Based Routing #
The reverse proxy lets you unify various independent microservices under one domain name.
- Path-based Routing: Requests to
example.com/api/v1go to the API microservice backend (e.g., Node.js), requests toexample.com/dashboardgo to the frontend application (e.g., React SPA), and requests toexample.com/staticare served directly from local file storage by Caddy. - Host-based Routing: Routes traffic based on DNS hostnames. Requests to
api.example.comgo to the API cluster, whileblog.example.comgoes to a WordPress CMS server.
4. Backend Security and Isolation #
By hiding the real IP addresses of your backend servers, you minimize the risk of targeted attacks like port scanning, OS-level backend exploitation, and direct DDoS attacks. Caddy can be configured to filter malicious requests, enforce rate limiting, and block suspicious IP addresses before those requests ever touch your application code.
5. Centralized Observability #
Instead of collecting access logs and metrics from dozens of fragmented application servers, you can collect traffic metrics, request latency statistics, and HTTP status codes (2xx, 3xx, 4xx, 5xx) from a single point: Caddy. This makes real-time monitoring of your entire infrastructure’s health easy.
The Request Lifecycle in Caddy #
To appreciate Caddy’s performance, we need to understand how an HTTP request flows through Caddy’s various internal components before being answered back to the client.
sequenceDiagram
autonumber
participant Client as Client (Browser)
participant Caddy as Caddy Server
participant Backend as App Backend (:3000)
Client->>Caddy: 1. TCP Connection & TLS Handshake (port 443)
Note over Caddy: Caddy validates SNI,<br/>decrypts the payload, & matches the route
Caddy->>Caddy: 2. Execute Middleware (log, rewrite, etc.)
Note over Caddy: Caddy selects the strongest upstream<br/>and opens a Keep-Alive connection
Caddy->>Backend: 3. Dial TCP & Send Modified HTTP Request (header_up)
Note over Backend: Backend processes the application logic
Backend-->>Caddy: 4. Send HTTP Response (body + header)
Note over Caddy: Caddy receives the response,<br/>modifies headers (header_down), & buffers data
Caddy-->>Client: 5. TLS Encryption & Send HTTP Response to ClientHere’s an in-depth explanation of each stage of the lifecycle above:
- TCP and TLS Handshake: The client initiates a TCP connection to port 443 of the Caddy server. Caddy responds by negotiating TLS. At this stage, Caddy detects the Server Name Indication (SNI) to determine which SSL certificate to present to the client. After the TLS handshake succeeds, a secure encrypted connection is established.
- HTTP Decryption and Parsing: Caddy decrypts the encrypted payload sent by the client into a standard HTTP request object (consisting of the HTTP method, URI path, query parameters, headers, and request body).
- Route Matching: Caddy evaluates your Caddyfile configuration blocks from top to bottom to find a matcher rule matching the request URI. If that route enables the
reverse_proxydirective, Caddy hands request handling over to the proxy module. - Initial Middleware Execution: Before data is forwarded to the backend, Caddy executes other configured middleware like path rewriting, access logging, or authentication.
- Upstream Selection: The proxy module evaluates the configured upstream list. Based on the chosen load balancing policy (e.g.,
least_conn), Caddy picks one healthy backend to handle this request. - Request Header Modification (
header_up): Caddy modifies the original headers from the client before sending them to the backend. Automatically, Caddy adds tracking headers likeX-Forwarded-Forto tell the backend the client’s real IP,X-Forwarded-Prototo inform about the original HTTPS scheme, and a customHostheader if needed. - Forwarding to the Backend (Dial & Send): Caddy sends the decrypted, modified HTTP request to the selected backend via an internal port (or Unix socket). Caddy uses connection pooling to avoid creating brand-new TCP sockets from scratch.
- Backend Processing: The backend receives the HTTP request from Caddy, queries the database, runs business logic, and returns an HTTP response (containing the status code like
200 OK, response headers, and a body of JSON/HTML data) back to Caddy. - Response Header Modification (
header_down): Caddy receives the response from the backend. Before forwarding it to the outside client, Caddy executes theheader_downrules. Here you can strip sensitive headers from the backend (likeX-Powered-By) or inject extra security headers (like HSTS, CSP, and CORS). - Final Compression and Encryption: If configured, Caddy compresses the response payload using modern algorithms (like
zstdorgzip). Finally, Caddy encrypts that data using the TLS session key negotiated in step 1, then sends it back over the TCP socket to the client.
Connection Management and Connection Pooling #
The key to Caddy’s high performance as a reverse proxy lies in its ability to manage TCP connections to backends efficiently using connection pooling.
The Naive Connection Problem (Without Connection Pooling) #
In traditional proxy servers that aren’t well configured, every incoming client request opens a new TCP connection to the backend, sends the request, receives the response, and immediately closes that TCP connection.
This pattern is very inefficient because each new TCP connection creation requires a 3-way handshake that costs network latency (round-trip time). If the backend uses HTTPS, the extra overhead of a TLS handshake (certificate negotiation and cryptographic key exchange) makes this latency worse. Under high traffic loads, the Caddy or backend server’s OS runs out of available local ports (ephemeral ports exhaustion), causing connection refused failures.
Caddy’s Solution: Connection Pooling #
Caddy enables connection pooling by default using persistent connections (HTTP Keep-Alive).
flowchart LR
A["Client A"] --> Caddy["Caddy"]
B["Client B"] --> Caddy
C["Client C"] --> Caddy
Caddy -. "Reusing Connections (Keep-Alive)" .-> Backend["Backend"]Here’s how it works:
- After Caddy finishes forwarding the response from the backend to Client A, Caddy does not close the TCP socket to that backend.
- That TCP connection is placed into a pool in an idle state.
- When Client B sends a new request a few milliseconds later, Caddy doesn’t need to do a new TCP handshake to the backend. Caddy directly grabs an idle connection from the pool and sends Client B’s request data immediately.
- If idle connections in the pool go unused for a certain time (controlled by the
keep_aliveparameter), Caddy closes them safely to free server memory.
Caddy also supports connection multiplexing using HTTP/2 for backend communication if the backend supports it, allowing dozens of concurrent requests over the same TCP socket simultaneously without Head-of-Line blocking issues.
Runtime Architecture: Caddy vs Nginx #
As infrastructure architects, we’re often faced with choosing between Nginx or Caddy for reverse proxy needs. To make a rational decision, we must understand the fundamental architectural differences under the hood of both systems.
1. Concurrency Model: Goroutine vs Event-Driven Worker #
- Nginx: Written in C and uses an asynchronous event-driven architecture managed by a set of worker processes. Usually, the number of Nginx worker processes matches the server’s physical CPU core count (e.g., 4 workers for a 4-core CPU). Each worker runs single-threaded and processes thousands of connections in turn using non-blocking system calls (like
epollon Linux orkqueueon macOS). This model is very memory-efficient (very small RAM footprint) but hard to configure if you need custom modules requiring heavy computation. - Caddy: Written in Go (Golang) and leverages Go’s built-in concurrency model called Goroutine. A Goroutine is a lightweight thread managed by the Go runtime scheduler, not directly by OS kernel threads. Caddy can spawn hundreds of thousands of concurrent goroutines with very efficient memory allocation (only about 2KB per goroutine initially). The Go scheduler automatically distributes these goroutines across all available CPU cores on your server. This model keeps Caddy’s internal code very clean, safe from memory vulnerabilities (memory safety), and able to use all CPU cores optimally without complex manual configuration.
2. Configuration Management and Reloading #
- Nginx: Depends on static configuration files (
nginx.conf). To change routing configuration or add a new domain, you must edit the file, validate syntax withnginx -t, then send a reload signal to the master process (nginx -s reload). This process is efficient but hard to integrate dynamically when your application needs to create new proxy routes automatically through program code (e.g., SaaS applications that create subdomains for new users instantly). - Caddy: Ships with a built-in REST API running on port
:2019. All of Caddy’s internal configuration is stored in JSON format. When you write a Caddyfile, Caddy actually translates it into this JSON format. You can change Caddy’s configuration (add proxy routes, change upstreams, modify headers) dynamically by sending HTTP POST JSON requests to Caddy’s API directly, without touching physical config files on disk and without reloading the process. This configuration change process happens in memory gracefully without dropping a single active client connection (zero-downtime hot reload).
Comprehensive Comparison Table #
| Evaluation Criteria | Caddy Server | Nginx (Open Source) |
|---|---|---|
| Programming Language | Go (Golang) | C |
| Concurrency Model | Goroutine (Go Runtime Scheduler) | Event-driven (Single-threaded worker via epoll) |
| Memory Safety | Very Safe (Garbage Collected, no buffer overflow) | Vulnerable if there’s a hole in third-party modules |
| SSL/TLS Certificates | Fully automatic (Built-in ACME engine) | Manual (Needs Certbot or external scripts) |
| Dynamic Configuration | Fully supported via native REST JSON API | Limited (Needs paid Nginx Plus or Lua module) |
| Active Health Checks | Supported out-of-the-box (Free) | Only available in Nginx Plus (Paid) |
| HTTP/3 (QUIC) | Fully supported by default | Needs manual compilation / experimental configuration |
| Memory Size | Slightly larger (10-30MB at idle) | Very small (2-5MB at idle) |
| Binary Distribution | Single static binary (No external dependencies) | Depends on system libraries (OpenSSL, PCRE) |
When to Choose Caddy as a Reverse Proxy #
To help you decide whether Caddy is the right choice for your application architecture, use the condition guide below.
You should CHOOSE Caddy if:
✓ You want to save deployment time with HTTPS automation without external Certbot scripts.
✓ Your application needs dynamic custom domain provisioning at runtime (SaaS platform) via API.
✓ Your developer team prefers concise, clean, human-readable configuration files.
✓ You need active health checking for backend servers without commercial license costs.
✓ You want to adopt modern protocols like HTTP/3 (QUIC) instantly without recompilation.
You should CONSIDER alternatives (like Nginx/HAProxy) if:
✗ Your server has very limited memory specs (like a 512MB RAM VPS) where every megabyte counts.
✗ You're required to follow strict corporate regulations mandating SSL certificates be managed offline by a dedicated security team.
✗ You need very specific load balancing modules not yet supported by Caddy's plugin ecosystem.
Summary #
- Key Definition: A reverse proxy acts on behalf of backend servers, hiding your internal architecture, while a forward proxy acts on behalf of clients to protect privacy or bypass network filters.
- Main Benefits: A protective layer to filter attacks, centralized TLS termination, workload distribution via load balancing, and unifying various microservices under one domain.
- Connection Pooling: Caddy keeps TCP sockets to backends open (Keep-Alive) so they can be reused by subsequent client requests, minimizing handshake latency.
- Caddy’s Advantages: Offers a modern goroutine architecture efficient on multi-core CPUs, dynamic configuration via REST API, zero-config HTTPS automation, and free enterprise features like active health checks.
← Previous: Reverse Proxy Next: Reverse Proxy Configuration →