Caddy reverse proxy WebSocket architecture diagram showing automatic protocol upgrades
Isometric diagram representing a Caddy reverse proxy node routing traffic to multiple backend services across an internal network.

How Caddy v2 handles WebSocket protocol upgrades

Caddy proxies WebSocket connections with no extra rules. You do not need manual Upgrade headers. The server passes client handshake headers to upstream apps unchanged. Once the backend returns an HTTP 101 status code, the proxy links both sides into an open TCP byte stream.

Older web proxies drop hop-by-hop headers by default. This breaks the RFC 6455 handshake unless teams write custom map blocks. The modern proxy fixes this problem. It checks upstream status codes and starts two-way streaming at once.

Architectural comparison: Caddy versus Nginx WebSocket proxying

Engineers moving from Nginx often hunt for missing lines. Nginx needs explicit map blocks to pass the Upgrade header. Without those blocks, Nginx drops the upgrade token. It falls back to plain HTTP requests. That extra step adds work to routine setup jobs.

The proxy spots the upstream 101 response on its own. It tunnels the TCP connection with no extra rules, as noted in the docs. As seen in our review of Caddy vs Nginx reverse proxy architecture, the engine uses clean defaults to avoid bugs.

This table compares key tasks across both systems for live traffic.

Operational parameterCaddy v2 reverse proxyNginx reverse proxy
Default WebSocket supportAutomatic and enabled nativelyRequires manual header directives
Upgrade header managementForwarded transparently by proxyMust map $http_upgrade to $connection_upgrade
Configuration lines needed1 directive line (reverse_proxy)4 to 6 lines of proxy_set_header syntax
Streaming flush controlNative flush_interval parameterproxy_buffering off directive
HTTP/2 and HTTP/3 supportAutomatic dual transport and ALPNManual http2 and http3 listen options
Zero-touch TLS automationBuilt-in ACME certificate managementExternal tools like Certbot required

Production Caddyfile configurations for single and multi-path services

Because the proxy handles protocol upgrades on its own, a basic setup needs only one block. An app serving web pages and live sockets needs a short config. The server proxies normal REST calls. It upgrades socket links smoothly.

Here is a working configuration file for a single app node.

example.com {
    # Automatic TLS provisioning via Let's Encrypt / ZeroSSL
    encode zstd gzip

    # Unified reverse proxy handling both HTTP and WebSockets
    reverse_proxy localhost:8080 {
        # Flush frames immediately without buffering delays
        flush_interval -1
    }
}

Large projects split static files, REST APIs, and socket apps. Standard API calls hit one app server. Real-time frames route to a separate socket backend. Route matchers keep these jobs cleanly apart.

app.example.com {
    encode zstd gzip

    # Route dedicated WebSocket connections on /socket/ path
    handle /socket/* {
        reverse_proxy ws-backend:9000 {
            flush_interval -1
            header_up X-Real-IP {remote_host}
        }
    }

    # Route standard API and web traffic
    handle {
        reverse_proxy web-backend:3000
    }
}

The handle directive keeps endpoints separate. Live socket traffic goes straight to upstream socket servers, and regular web calls flow to primary app instances.

Docker Compose networking and persistent socket streaming

Docker isolates proxies from backend apps. When running in Docker, place services on a shared private bridge. As covered in our guide on pinning container images by digest, predictable image versions ensure network services stay stable. Do not bind container ports to host ports unless needed.

Here is a clean Docker Compose file running Caddy with a Node.js socket app.

# Modern Compose syntax (top-level version omitted per Compose v2 spec)
services:
  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp" # HTTP/3 QUIC
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - webnet

  realtime-service:
    image: my-websocket-app:latest
    restart: unless-stopped
    expose:
      - "8080"
    networks:
      - webnet

networks:
  webnet:
    driver: bridge

volumes:
  caddy_data:
  caddy_config:

The container resolves realtime-service:8080 through internal Docker DNS. It handles public TLS with modern ciphers, as shown in our guide on TLS 1.3 vs TLS 1.2 handshake speed, and keeps backend tunnels open.

Tuning stream timeouts and eliminating frame buffer delays

Live apps need low lag. Standard web proxies hold network packets to group bytes. For small socket frames, holding bytes adds lag. The proxy provides the flush_interval knob to control byte flow.

The directive flush_interval -1 tells the proxy to flush payload frames to clients right after each write, as noted in the docs. Setting flush_interval to -1 disables proxy buffering and delivers frames immediately. This setting prevents frame delay across chat tools, shared editors, and game servers. Also, idle connections risk sudden drops if ping frames stay absent.

RFC 6455 defines ping and pong frames to keep quiet links fresh. The proxy passes these control frames through cleanly. App servers should send regular ping frames to prevent intermediaries from dropping inactive connections. This routine stops cloud firewalls and routers from closing idle links.

Troubleshooting checklist for failed WebSocket connections

When socket handshakes fail through an intermediary proxy, the client connection terminates abruptly without application payload data. Because this disconnect gives no context, engineers must check steps in order. Follow this checklist to find errors.

Diagnostic checkpointCommon root causeResolution procedure
HTTP 400 Bad RequestUpstream missing RFC 6455 headersVerify upstream service receives Sec-WebSocket-Key and Version
HTTP 502 Bad GatewayUpstream container unreachable or downVerify container health and Docker network DNS resolution
Handshake stalls at 200 OKBackend treated socket as plain HTTPCheck upstream framework router for WebSocket upgrade handler
Silent disconnect on quiet streamFirewall or proxy idle connection timeoutImplement application-level ping/pong heartbeats on idle streams
Mixed content security errorBrowser on HTTPS requesting ws://Ensure client application requests secure wss:// protocol
Origin validation failureCORS or allowed origins mismatchConfigure backend allowed origins to match public domain

Test handshakes from your terminal using cURL with upgrade headers.

curl -i -N \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  https://example.com/socket/

A healthy server returns HTTP/1.1 101 Switching Protocols and Upgrade: websocket. Once verified, your live socket proxy is ready for production.

Frequently asked questions about Caddy WebSocket proxying

Does Caddy v2 support WebSockets out of the box?

Yes. Caddy v2 handles WebSocket protocol upgrades automatically without extra configuration.

Why does Nginx require extra headers while Caddy does not?

Nginx clears hop-by-hop headers by default. Caddy inspects protocol switch responses and streams TCP bytes directly.

How do you route WebSockets on a specific subpath in Caddy?

Use a path matcher like reverse_proxy /ws/* localhost:8080 to isolate real-time traffic.

Sources and technical documentation