Caddy’s API allows you to reload its configuration without dropping existing HTTP connections.

Here’s Caddy serving a basic static site and handling a live connection while its configuration is updated:

# Terminal 1: Start Caddy with initial config
$ echo '{ "apps": { "http": { "servers": { "myserver": { "listen": [":80"], "routes": [ { "handle": [ { "handler": "static_file_server", "root": "/var/www/html" } ] } ] } } } } }' > caddy.json
$ caddy run --config caddy.json --api

# Terminal 2: Simulate a client making a request
$ curl http://localhost/
# Output: (empty, assuming /var/www/html is empty for now)

# Terminal 3: Update Caddy config to serve a different path and trigger reload
$ echo '{ "apps": { "http": { "servers": { "myserver": { "listen": [":80"], "routes": [ { "handle": [ { "handler": "static_file_server", "root": "/var/www/html/new" } ] } ] } } } } }' > caddy.json
$ curl localhost:2019/config/load -X POST --data-binary @caddy.json
# Output: (empty, indicating success)

# Terminal 2: Make another request, now hitting the new path
$ curl http://localhost/
# Output: (content from /var/www/html/new, if it exists)

The core problem Caddy’s API solves here is graceful configuration reloads. Traditionally, updating a web server’s configuration often meant restarting the entire process, which tears down all active TCP connections. This is disruptive for users and bad for application availability. Caddy’s API provides a programmatic way to tell the running Caddy instance to apply a new configuration without terminating its existing network listeners or active connections.

Internally, when you send a POST request to /config/load on the Caddy API endpoint (defaulting to localhost:2019), Caddy performs a multi-step process:

  1. Validation: It first parses and validates the new configuration JSON. If there are syntax errors or invalid directives, it will return an error response, and the current configuration remains untouched.
  2. New Server Initialization: If validation passes, Caddy starts initializing the new set of servers and their associated listeners based on the updated configuration. These new listeners are typically bound to the same addresses and ports as the old ones.
  3. Connection Draining (Implicit): Crucially, Caddy does not immediately shut down the old listeners. Existing connections that are already established on the old listeners continue to be served by the old configuration.
  4. Switchover: Once the new listeners are fully initialized and ready to accept traffic, Caddy atomically swaps its internal routing tables. New incoming requests will now be directed to the new configuration.
  5. Old Listener Shutdown: After the switchover, Caddy begins gracefully shutting down the old listeners. This means no new connections are accepted on the old listeners, but existing connections are allowed to complete their current request-response cycle. It doesn’t abruptly close them.

The key levers you control are the API endpoint and the configuration payload. The API endpoint is typically localhost:2019 by default, but can be changed in Caddy’s admin configuration block. The payload is a JSON object mirroring Caddy’s Caddyfile structure, but expressed in JSON. You can load a full new configuration or, with more advanced API usage, partial updates.

The most surprising thing is how Caddy achieves this seamless transition without needing explicit "drain" commands for older connections. It’s not that it waits for connections to finish; it’s that the old listeners remain active and responsive for their existing connections until they naturally close or complete their current request. The switch is effectively a pointer change for incoming requests, and the old resources are cleaned up once they are no longer actively serving anything new.

The next step is understanding how to manage more complex scenarios like rolling deployments across multiple Caddy instances using the API.

Want structured learning?

Take the full Caddy course →