Common Errors #
Knowing the most frequently occurring errors and understanding how to solve them tactically can save hours during troubleshooting. As a modern web server, Caddy gives fairly clear error messages, but often the root cause lies in the interaction between Caddy, the OS configuration, the network, or your backend application. This article compiles a list of the most common errors Caddy users face in production environments along with proven solution steps.
Connection & TLS Troubleshooting Diagram #
Before diving into the technical details of each error, you can use the decision tree flowchart below as a quick guide to isolate network and SSL/TLS certificate problems in Caddy:
flowchart TD
Start["Client Experiences an Error"] --> Type{"Problem Type?"}
Type -->|"Connection Failed / Timeout"| PortCheck{"Port 80 or 443 Open?"}
PortCheck -- No --> Firewall["Check Firewall, Security Group & Port Binding (lsof)"]
PortCheck -- Yes --> BackendCheck{"Backend Service Active?"}
BackendCheck -- No --> RunBackend["Run the Backend & Check the Port (curl / ss)"]
BackendCheck -- Yes --> ProxyConfig["Check the reverse_proxy Configuration & Caddyfile Target Port"]
Type -->|"TLS / SSL Error"| DNSCheck{"DNS Resolves to the Server IP?"}
DNSCheck -- No --> UpdateDNS["Update the Domain A/AAAA DNS Records"]
DNSCheck -- Yes --> ACMEType{"ACME Challenge Type?"}
ACMEType -->|HTTP-01| HTTPPort["Make Sure Port 80 is Open to the Internet & Temporarily Disable Cloudflare Proxy"]
ACMEType -->|DNS-01| DNSToken["Check the DNS API Token & Credential File Write Permissions"]
ACMEType -->|Local PKI| LocalTrust["Run caddy trust on the Client Machine"]
style Start stroke:#0288d1,stroke-width:2px
style Type stroke:#7b1fa2,stroke-width:2px1. Port Already in Use #
Error Symptoms #
When trying to run Caddy manually or starting the service via systemd, you see the following error messages in the system log:
Error: listen tcp :443: bind: address already in use
Error: listen tcp :80: bind: address already in use
Main Causes #
Every IP address and port on an OS can only be bound by one application process at a time. The message above indicates another application (like Nginx, Apache HTTPD, HAProxy, or another Caddy instance running in the background) already occupies the HTTP (80) or HTTPS (443) port.
Diagnosis & Solution Steps #
You need to identify which process uses that port and stop it before starting Caddy:
# 1. Find the process name and PID occupying ports 80 and 443
sudo lsof -i :80
sudo lsof -i :443
# Alternative using the ss command (faster on modern Linux)
sudo ss -tlnp | grep -E ':80|:443'
# Example output:
# LISTEN 0 128 0.0.0.0:443 0.0.0.0:* users:(("nginx",pid=1234,fd=6))
# 2. Stop the conflicting service if recognized
sudo systemctl stop nginx # If the system uses Nginx
sudo systemctl stop apache2 # If the system uses Apache
# 3. If it's a wild Caddy instance not managed by systemd, force stop it
sudo kill -9 1234 # Replace 1234 with the PID from the tracking above
# 4. Restart our Caddy service
sudo systemctl start caddy
2. Permission Denied on Ports < 1024 #
Error Symptoms #
When running the Caddy binary using a regular (non-root) user, Caddy refuses to start and emits the log:
Error: listen tcp :443: bind: permission denied
Main Causes #
UNIX-based OSes (including Linux and macOS) restrict port binding below 1024 (known as privileged ports or well-known ports) to users with high privileges (root or superuser). This is a security measure to prevent regular users from impersonating official system services.
Diagnosis & Solution Steps #
Running a production web server directly as the root user is strongly discouraged to comply with the principle of least privilege. Instead, use one of the following solutions:
# Solution 1: Give special capabilities (Capabilities) to the Caddy binary
# This allows the Caddy binary to bind low ports without full root access
sudo setcap cap_net_bind_service=+ep $(which caddy)
# Verify whether the capability was successfully added
getcap $(which caddy)
# Correct output: /usr/bin/caddy = cap_net_bind_service+ep
# Solution 2: If using Systemd (Highly recommended for production)
# Make sure the systemd unit file (/etc/systemd/system/caddy.service) has the following options:
# [Service]
# User=caddy
# Group=caddy
# AmbientCapabilities=CAP_NET_BIND_SERVICE
# CapabilityBoundingSet=CAP_NET_BIND_SERVICE
3. TLS Certificate Error (ACME Challenge Failed) #
Error Symptoms #
The SSL/TLS certificate fails to be issued, browsers show the Your connection is not private warning, and the Caddy log records:
Error: obtaining certificate: ...
Error: ACME challenge failed
Error: HTTP-01 challenge: could not connect to CA
Main Causes #
Caddy uses the ACME protocol automatically to request certificates from Let’s Encrypt or ZeroSSL. The standard challenge used is HTTP-01, which requires the ACME server from the outside internet to reach Caddy on port 80 at the domain you registered. Failure happens if the domain doesn’t point to your server IP, port 80 is blocked by a firewall, or a CDN proxy cuts the connection.
Diagnosis & Solution Steps #
Do the following structured investigation to find the bottleneck:
# 1. Make sure the DNS records (A and AAAA) point exactly to your server's public IP
dig +short example.com
# The output must be your server's public IP. If empty, update your DNS records.
# 2. Check whether ports 80 and 443 are open in the local firewall (UFW / IPTables)
sudo ufw status
# If blocked, allow HTTP & HTTPS traffic:
sudo ufw allow proto tcp from any to any port 80,443
# 3. Test whether your server's port 80 can be reached from the outside network
# You can use curl from another machine or online tools like Let's Debug
curl -I http://example.com/.well-known/acme-challenge/test-probe
# 4. CDN Scenario (Cloudflare, etc.)
# If you enable the "Proxy" option (orange cloud) in Cloudflare, Cloudflare blocks the HTTP-01 challenge.
# Solution: Turn off the Cloudflare proxy (switch to DNS Only / gray cloud) temporarily,
# or switch to using the DNS-01 Challenge with the caddy-dns plugin.
4. 502 Bad Gateway #
Error Symptoms #
Clients receive the HTTP 502 Bad Gateway status code when trying to load a proxied page, and the Caddy log shows a dialing upstream error:
HTTP/1.1 502 Bad Gateway
Log: [error] dial tcp 127.0.0.1:3000: connect: connection refused
Main Causes #
Caddy acts as a reverse proxy forwarding requests to backend applications (like Node.js, Python FastAPI, Go, etc.). The 502 status means Caddy can’t establish a TCP or UNIX socket connection with that backend server. This usually happens because the backend is down, the port number is wrong, or there’s a socket permission problem.
Diagnosis & Solution Steps #
Make sure the backend is running and listening on the port matching the Caddyfile configuration:
# 1. Test the direct connection to the backend from inside the server
curl -I http://localhost:3000/health
# If it returns "connection refused", the backend is dead.
# 2. Check the backend process status
sudo systemctl status my-backend-app
# Or if using PM2 for Node.js:
pm2 status
# 3. Check the port occupied by the backend
sudo ss -tlnp | grep :3000
# Make sure the binding address is 127.0.0.1 or 0.0.0.0 on the correct port.
# 4. UNIX Socket Scenario (File Permissions)
# If using a socket file (e.g., php-fpm), make sure the caddy user has read-write access:
# ANTI-PATTERN: Setting the socket file with too-strict permissions (chmod 600) so Caddy gets blocked
# CORRECT: Give group ownership to the caddy user
# chown php-user:caddy /var/run/php/php-fpm.sock
# chmod 660 /var/run/php/php-fpm.sock
5. 504 Gateway Timeout #
Error Symptoms #
Clients receive the HTTP 504 Gateway Timeout response code after waiting a while, and the Caddy log records:
HTTP/1.1 504 Gateway Timeout
Log: [error] context deadline exceeded (client timeout while reading response headers)
Main Causes #
The backend received the request from Caddy, but took too long to produce a response (exceeding Caddy’s default timeout limit). This often happens with heavy backend operations, like large data exports, image processing, or unoptimized database queries.
Diagnosis & Solution Steps #
You need to adjust the timeout limits in the transport http section inside the Caddyfile reverse_proxy block, and analyze which endpoints are slow:
# Setting timeout limits explicitly in the Caddyfile
example.com {
reverse_proxy localhost:3000 {
transport http {
# Set the response header read timeout (e.g., 60 seconds)
response_header_timeout 60s
# Set the response body read timeout
read_timeout 120s
# Set the request send timeout to the backend
write_timeout 60s
}
}
}
# Track which endpoints trigger high latency via the Caddy access log
cat /var/log/caddy/access.log | \
jq 'select(.duration > 5.0) | {timestamp: .ts, path: .request.uri, duration: .duration, status: .status}'
6. Syntax Error in the Caddyfile #
Error Symptoms #
Caddy refuses to reload the configuration or fails to start, showing a confusing parse error message:
Error: parsing Caddyfile: Caddyfile:15 - Error during parsing: unrecognized directive: rverse_proxy
Main Causes #
There’s a typo in the directive name, unpaired curly braces {}, or a domain name matcher written not according to Caddyfile syntax standards.
Diagnosis & Solution Steps #
Never do configuration loading directly in production without validating it first:
# 1. Run Caddy's built-in verification utility
caddy validate --config /etc/caddy/Caddyfile
# Output if an error occurs:
# parsing Caddyfile tokens: /etc/caddy/Caddyfile:15 - Error during parsing: unrecognized directive: rverse_proxy
# 2. Immediately open the file and fix line 15 (change rverse_proxy to reverse_proxy)
# 3. Run the JSON adaptation to make sure the Caddyfile translates perfectly
caddy adapt --config /etc/caddy/Caddyfile > /dev/null
7. TLS Handshake Error (tls: no certificate available) #
Error Symptoms #
HTTPS connections to the server fail completely at the SSL/TLS handshake level. Clients see the SSL_ERROR_NO_CYPHER_OVERLAP error and the Caddy log records:
Error: TLS handshake error from 203.0.113.1:12345: tls: no certificate available
Main Causes #
Caddy receives a TLS handshake request for a certain domain (via SNI identification), but Caddy has no matching certificate in its local storage for that domain. This happens if the domain was just added and the certificate issuance process hasn’t finished, or Caddy is configured to block automatic issuance for that domain (e.g., not registered in a Named Matcher).
Diagnosis & Solution Steps #
Make sure the certificate was successfully issued and stored correctly:
# 1. Check Caddy's local certificate storage folder
# For standard systemd deployments, it's located at /var/lib/caddy/
sudo ls -la /var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/
# 2. Check whether your domain's certificate is in the list above.
# If not, check Caddy's initial startup log to see why the ACME process is delayed:
sudo journalctl -u caddy -b | grep -E "obtaining|certificate|ACME"
# 3. Expired or corrupt certificate scenario
# If the certificate file is damaged, you can move it to a backup folder and force Caddy to request again:
sudo mv /var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/example.com /tmp/example.com-backup
sudo systemctl reload caddy
8. Too Many Redirects (ERR_TOO_MANY_REDIRECTS) #
Error Symptoms #
The browser shows the too-many-redirects error screen (infinite redirect loops) when trying to load the website.
Main Causes #
Caddy by default redirects all HTTP traffic (port 80) to HTTPS (port 443). If Caddy sits behind a CDN or Load Balancer (like Cloudflare, AWS ALB, or F5 BIG-IP) doing SSL termination at the edge level and forwarding connections back to Caddy in plain HTTP form (port 80), then:
- The CDN receives HTTPS, then forwards HTTP to Caddy.
- Caddy sees HTTP, then responds with a redirect command to HTTPS.
- The CDN receives that redirect, sends it back to the client browser, and the cycle repeats endlessly.
Diagnosis & Solution Steps #
You must tell Caddy to trust the original protocol headers sent by the CDN/Load Balancer using the trusted_proxies configuration:
# Test the redirect path in detail using curl
curl -L -v https://example.com 2>&1 | grep -E "Location:|< HTTP"
# If you see Location lines bouncing back and forth HTTPS-HTTP-HTTPS, a loop is happening.
# Solution: Register your Load Balancer IPs in the Caddyfile global options
{
servers {
trusted_proxies static 10.0.0.0/8 192.168.1.0/24 # Replace with your Load Balancer IP CIDR
}
}
example.com {
# With trusted_proxies active, Caddy recognizes the X-Forwarded-Proto header
# and won't do an HTTP redirect if the real client is actually using HTTPS
reverse_proxy localhost:3000
}
9. WebSocket Connection Failed #
Error Symptoms #
Web applications relying on real-time connections (like Socket.io, Chat, or dashboards) fail to load interactive features. The browser shows an error in the developer console:
WebSocket connection to 'wss://example.com/socket.io/' failed: Unexpected response code: 400
Main Causes #
The WebSocket protocol requires a special handshake mechanism (Connection Upgrade) from HTTP/1.1 to WebSocket. On traditional web servers like Nginx, you must configure the Upgrade and Connection headers manually. In Caddy, this upgrade handling is actually automatic by default. However, failure can happen if your backend rejects certain header values modified by the gateway, or because the connection is unilaterally cut by network timeout rules.
Diagnosis & Solution Steps #
Do a manual WebSocket handshake test to detect the backend response:
# 1. Send a handcrafted WebSocket upgrade request using curl
curl -i -N \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Host: example.com" \
-H "Origin: https://example.com" \
-H "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
-H "Sec-WebSocket-Version: 13" \
https://example.com/socket.io/
# The correct result (Status 101 Switching Protocols):
# HTTP/1.1 101 Switching Protocols
# Upgrade: websocket
# Connection: Upgrade
If it returns a 400 or 404 status, check whether your backend is actually listening for WebSocket connections on that URL path. In the Caddyfile, make sure to turn off compression if it interferes with WebSocket packet transmission:
example.com {
# If your WebSocket application has problems with compression,
# exclude the WebSocket path from the global encode compression
encode gzip zstd
reverse_proxy localhost:3000 {
# websocket handshake is forwarded automatically by Caddy
}
}
10. Rate Limit Keeps Triggering (False Positive 429) #
Error Symptoms #
Trusted clients or even internal developers suddenly get blocked with the 429 Too Many Requests response status.
Main Causes #
You configured the caddy-ratelimit module using the {remote_host} detection key. However, because the Caddy gateway sits behind an external proxy/load balancer and you haven’t registered that proxy’s IP in the global trusted_proxies option, Caddy identifies the load balancer IP as a single client IP. As a result, all global traffic is counted as requests from one user and immediately triggers blocking.
Diagnosis & Solution Steps #
Make sure the real public client IP detection works correctly in Caddy:
# 1. Run curl and check the X-RateLimit header if present
curl -I https://api.example.com/
# 2. Check the Caddy access log to see whether the client IP is recorded as the proxy's local IP
# If the log shows IPs like 10.0.x.x or 172.x.x.x instead of the client's public IP,
# you're experiencing an origin IP misconfiguration.
# Caddyfile fix:
{
servers {
# Enable client IP tracking through trusted proxies
trusted_proxies static 10.0.0.0/8
}
}
api.example.com {
@trusted_devs remote_ip 203.0.113.50 # Developer's public IP
rate_limit {
zone general_api {
key {remote_host} # Now identifies the real public client IP thanks to trusted_proxies
window 1m
events 60
exclude @trusted_devs # Skip limits for developers
}
}
reverse_proxy localhost:3000
}
11. Caddy Can’t Write Certificates (Permission Denied) #
Error Symptoms #
The SSL certificate issuance process stalls completely, and when checking the log you see a permission denied message:
Error: open /var/lib/caddy/.local/share/caddy/certificates/...: permission denied
Main Causes #
The Caddy certificate data storage directory (/var/lib/caddy/ or /var/lib/caddy/.local/) has wrong ownership or file permission rights. This usually happens when you once ran Caddy using the sudo caddy run command as the root user, so the system created new certificate folders owned by root, which then can’t be accessed again by the regular system user caddy when running via systemd.
Diagnosis & Solution Steps #
Restore the ownership permissions of the Caddy data folder to the system caddy user:
# 1. Check the ownership of the Caddy storage directory
ls -la /var/lib/caddy/
# If the owner is root:root, fix it immediately.
# 2. Restore ownership recursively to the caddy user and group
sudo chown -R caddy:caddy /var/lib/caddy/
# 3. Set safe directory permissions (only the owner has full access)
sudo chmod -R 750 /var/lib/caddy/
# 4. Test the folder write capability using the caddy user identity
sudo -u caddy touch /var/lib/caddy/.local/share/caddy/test-permission && echo "Write Permission: OK"
# 5. Clean up the test file and restart Caddy
sudo -u caddy rm /var/lib/caddy/.local/share/caddy/test-permission
sudo systemctl restart caddy
12. Caddy Suddenly Stops (OOM Killer) #
Error Symptoms #
The Caddy server dies suddenly without any error record in Caddy’s final log. When checking the service status, you see the Killed status or Main process exited, code=killed, status=9/KILL.
Main Causes #
The Caddy process was force-killed by a Linux kernel internal feature called the OOM (Out Of Memory) Killer. This happens when the server runs out of physical RAM and swap memory due to very high connection load, memory leaks in custom plugin modules, or because another backend application on the same server consumes the entire RAM capacity.
Diagnosis & Solution Steps #
Track the kernel event records to verify whether the forced memory kill actually happened:
# 1. Check the system dmesg log to detect OOM
sudo dmesg -T | grep -i -E "oom|killed"
# Example output: Out of memory: Killed process 5678 (caddy) total-vm:4194304kB, anon-rss:2097152kB
# 2. Check the general system log
sudo journalctl -k | grep -i -E "oom|killed"
# 3. Short-term Solution: Configure Systemd to auto-restart Caddy if it crashes
sudo systemctl edit caddy
Add the following override configuration block in the Systemd editor:
[Service]
# Force Systemd to restart Caddy if it dies abnormally
Restart=on-failure
# Wait 5 seconds before trying to restart
RestartSec=5s
# Limit automatic restart attempts so it doesn't loop endlessly if the damage is permanent
StartLimitIntervalSec=300
StartLimitBurst=5
13. Let’s Encrypt Rate Limit Exceeded #
Error Symptoms #
When adding many subdomains at once for the first time on a new server, Caddy suddenly shows the ACME rate limit error message:
Error: rateLimited: too many certificates already issued for exact set of domains: ...
Main Causes #
Let’s Encrypt applies strict rate limits to protect their infrastructure. The most common limit is 50 new certificates per parent domain per week. If you repeatedly delete and recreate Caddy Docker containers or repeatedly reload configurations with small domain name changes while experimenting, you’ll quickly exceed this limit.
Diagnosis & Solution Steps #
Use a staging test environment during the configuration trial phase before switching to production certificates:
# Caddyfile fix for the testing / experimentation phase:
{
# Use the Let's Encrypt staging endpoint (no limits as strict as production)
acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}
test.example.com {
file_server
}
# Publicly track the certificate issuance history for your domain
curl -s "https://crt.sh/?q=example.com&output=json" | \
jq '[.[] | select(.not_before > (now - 604800 | todate))] | length'
# The output shows how many certificates were issued in the last 7 days.
If you’ve already been hit by the rate limit, you can temporarily work around it by switching the issuing authority (CA Issuer) to ZeroSSL which doesn’t use Let’s Encrypt’s rate limits:
# Switch to the ZeroSSL ACME Directory
{
acme_ca https://acme.zerossl.com/v2/DV90
}
14. Memory Usage Keeps Increasing #
Error Symptoms #
The Caddy server experiences a constant RAM usage increase (gradual memory growth) for days without ever returning to normal levels, even after peak hours end.
Main Causes #
A memory leak is usually caused by third-party plugins or modules (like third-party logging plugins, custom auth, or custom L4 proxies) that don’t manage Go memory allocation cleanly. On Caddy’s core engine itself, memory leaks are very rare.
Diagnosis & Solution Steps #
You need to monitor memory usage periodically and isolate the cause:
# 1. Monitor the Caddy process RSS (Resident Set Size) memory consumption in real time
watch -n 10 'ps aux | grep caddy | grep -v grep | awk "{print \$6/1024 \" MB - RAM Usage\"}"'
# 2. Track whether there are file descriptor leaks holding memory allocations
sudo lsof -p $(pgrep caddy) | wc -l
# 3. Isolation Solution:
# If you suspect a certain plugin as the culprit, recompile the Caddy binary
# without that plugin gradually to verify whether RAM consumption stabilizes.
As a temporary workaround in production environments while debugging plugin code, you can schedule a daily cron graceful reload process. Unlike other web servers, caddy reload safely cleans old runtime memory allocations (atomic swap) without cutting off active client connections:
# Add to the root crontab for a graceful reload every day at 4 AM
0 4 * * * /usr/bin/caddy reload --config /etc/caddy/Caddyfile > /dev/null
Summary #
- Port Binding Conflict — The
address already in useerror is solved by tracking the process occupying the port withsudo lsof -i :80,443then killing that process.- Privileged Ports — Running Caddy as non-root on ports 80/443 requires giving kernel capabilities via
sudo setcap cap_net_bind_service=+ep $(which caddy).- ACME Challenge — SSL/TLS certificate failures on the HTTP-01 challenge are often because port 80 is blocked by a firewall or the domain DNS record hasn’t pointed to the server IP yet.
- 502 Bad Gateway — Indicates Caddy lost the connection to the downstream backend. Make sure the backend service is alive and listening on the right port.
- Redirect Loop — Caused by an HTTPS termination clash between the CDN/load balancer and Caddy. Solve it by registering the load balancer IP in the
trusted_proxiesblock.- Atomic Config Swap — Caddy supports reloads without downtime. Do defensive validation first using the
caddy validatecommand before applying it in production.