# How Caddy Dynamic Config Loading Works: Runtime Configuration Without Restarts

> Learn how Caddy dynamic config loading updates your server configuration without restarts using its admin API and external runtime fetching. Maximize uptime and flexibility.

- Repository: [Caddy/caddy](https://github.com/caddyserver/caddy)
- Tags: internals
- Published: 2026-03-03

---

**Caddy dynamic config loading enables the server to replace or augment its running configuration without process restarts through a combination of the admin API, the ConfigLoader abstraction, and configurable load delays that fetch external configurations at runtime.**

Caddy dynamic config loading is a core feature of the caddyserver/caddy repository that allows the web server to update its behavior on the fly. This capability eliminates downtime during configuration changes by leveraging a modular architecture built around the admin API and pluggable configuration loaders. The system is designed to be thread-safe and extensible, supporting everything from simple HTTP POST requests to complex remote configuration polling.

## The Admin API: /load and /adapt Endpoints

The admin API provides the primary interface for Caddy dynamic config loading through two key endpoints registered in **[[`caddyconfig/load.go`](https://github.com/caddyserver/caddy/blob/main/caddyconfig/load.go)](https://github.com/caddyserver/caddy/blob/master/caddyconfig/load.go)**. The `adminLoad` module routes requests to `handleLoad` and `handleAdapt` handlers that process new configuration payloads.

The `handleLoad` function (lines 73‑84) reads the request body and optionally adapts it based on the `Content-Type` header via `adaptByContentType`. It then invokes `caddy.Load(body, forceReload)`, which forwards the JSON to `changeConfig` as implemented in **[[`caddy.go`](https://github.com/caddyserver/caddy/blob/main/caddy.go)](https://github.com/caddyserver/caddy/blob/master/caddy.go)** (lines 15‑23). This triggers `unsyncedDecodeAndRun`, which validates, marshals, and activates the new configuration.

To force a reload even when the configuration is identical, include the header `Cache-Control: must-revalidate` in your request. The `/adapt` endpoint performs the same content negotiation without applying the configuration, returning the adapted JSON for inspection.

## The ConfigLoader Interface

Dynamic loading is driven by the `ConfigLoader` interface defined in **[[`caddy.go`](https://github.com/caddyserver/caddy/blob/main/caddy.go)](https://github.com/caddyserver/caddy/blob/master/caddy.go)** at lines 671‑676:

```go
type ConfigLoader interface {
    LoadConfig(Context) ([]byte, error)
}

```

Modules implementing this interface can fetch configurations from any external source. The built-in HTTP loader resides in **[[`caddyconfig/httploader.go`](https://github.com/caddyserver/caddy/blob/main/caddyconfig/httploader.go)](https://github.com/caddyserver/caddy/blob/master/caddyconfig/httploader.go)** and is registered via `caddy.RegisterModule(HTTPLoader{})` at line 29. Its `LoadConfig` method constructs HTTP requests, handles retries, reads response bodies, and adapts payloads according to the `Content-Type` header.

You reference loaders in your configuration using the `admin.config.load` path:

```json
{
    "admin": {
        "config": {
            "load": {
                "module": "caddy.config_loaders.http",
                "url": "https://example.com/caddy.json"
            }
        }
    }
}

```

## ConfigSettings: Controlling When and What to Load

The `AdminConfig` structure contains a nested `ConfigSettings` type defined in **[[`admin.go`](https://github.com/caddyserver/caddy/blob/main/admin.go)](https://github.com/caddyserver/caddy/blob/master/admin.go)** at lines 27‑45. Two fields orchestrate the loading behavior:

- **`load`** (`LoadRaw`) – Raw JSON defining a `ConfigLoader` module instance.
- **`load_delay`** (`LoadDelay`) – A `Duration` specifying when to execute the loader.

When Caddy finishes provisioning the core in `finishSettingUp` (lines 601‑667 of **[[`caddy.go`](https://github.com/caddyserver/caddy/blob/main/caddy.go)](https://github.com/caddyserver/caddy/blob/master/caddy.go)**), it checks for configured loaders:

```go
if cfg != nil && cfg.Admin != nil && cfg.Admin.Config != nil && cfg.Admin.Config.LoadRaw != nil {
    val, err := ctx.LoadModule(cfg.Admin.Config, "LoadRaw")
    // …
}

```

The execution path diverges based on the delay value (lines 626‑656):

- **`LoadDelay > 0`**: A goroutine starts a `time.Timer`. After the delay, it calls `val.(ConfigLoader).LoadConfig(ctx)`. Success triggers `runLoadedConfig`; failures trigger retries after the same delay.
- **`LoadDelay == 0`**: The loader runs synchronously during startup, with the configuration applied in a separate goroutine to prevent deadlocks with the admin server.

This delay mechanism prevents tight infinite loops by ensuring that dynamically loaded configurations requesting further loads must specify a positive delay.

## How Configuration Changes Propagate

Once a loader fetches raw bytes via `LoadConfig`, the propagation follows this sequence:

1. **`runLoadedConfig`** receives the bytes and invokes `unsyncedDecodeAndRun`.
2. **Parsing** converts the JSON into a `*Config` structure.
3. **Provisioning** initializes modules within the new configuration.
4. **Context swapping** updates `currentCtx` under the protection of `rawCfgMu` and `currentCtxMu`.
5. **Lifecycle management** stops old applications and starts new ones while the admin API continues serving requests.

All state transitions occur under mutex protection to guarantee thread-safety during concurrent configuration updates.

## Practical Examples

### Loading Configuration via the Admin API

Push a new configuration directly to the running server:

```bash
curl -X POST http://localhost:2019/load \
  -H "Content-Type: application/json" \
  -H "Cache-Control: must-revalidate" \
  -d @new-config.json

```

For non-JSON payloads like Caddyfiles, set the `Content-Type` to `text/caddyfile` or specify an adapter in the request body.

### Configuring Remote HTTP Loading with Delay

Configure Caddy to fetch and apply a remote configuration 10 seconds after startup:

```json
{
  "admin": {
    "config": {
      "load": {
        "module": "caddy.config_loaders.http",
        "url": "https://config.example.com/caddy.json",
        "method": "GET",
        "timeout": "30s"
      },
      "load_delay": "10s"
    }
  }
}

```

Caddy waits the specified duration, fetches the remote JSON, and replaces its running configuration. Failed loads retry after each delay interval until successful or until the process terminates.

### Implementing a Custom Loader

Create a Go module that implements the `ConfigLoader` interface, register it with `caddy.RegisterModule`, and reference it in `admin.config.load` using your module's registered name.

## Summary

- **Caddy dynamic config loading** eliminates restart requirements by applying configurations at runtime through the admin API or automated loaders.
- The **`ConfigLoader`** interface abstracts configuration sources, with built-in HTTP support in **[`caddyconfig/httploader.go`](https://github.com/caddyserver/caddy/blob/main/caddyconfig/httploader.go)**.
- **`ConfigSettings`** fields (`load` and `load_delay`) control loader execution timing and prevent recursive load loops.
- The **`/load`** and **`/adapt`** endpoints in **[`caddyconfig/load.go`](https://github.com/caddyserver/caddy/blob/main/caddyconfig/load.go)** provide the HTTP interface for manual configuration pushes.
- Thread-safe propagation relies on mutex-protected context swapping in **`unsyncedDecodeAndRun`** to ensure zero-downtime transitions.

## Frequently Asked Questions

### What is the difference between the /load and /adapt admin API endpoints?

The **`/load`** endpoint accepts a configuration payload, adapts it if necessary, and immediately applies it to the running server via `changeConfig`. The **`/adapt`** endpoint performs the same content-type negotiation and adaptation but returns the resulting JSON without applying it, allowing you to preview or validate configurations before activation.

### How does Caddy prevent infinite loops when dynamically loading configurations?

The **`load_delay`** field in `ConfigSettings` prevents tight loops by requiring a positive delay duration for any configuration that triggers subsequent loads. When `load_delay` is zero, the loader runs synchronously during startup. When positive, a timer goroutine manages the load, and any nested load requests must specify their own positive delays, breaking potential infinite recursion.

### Can I use Caddy dynamic config loading with a custom configuration source?

Yes. Implement the **`ConfigLoader`** interface with a `LoadConfig(Context) ([]byte, error)` method to fetch from any source—databases, message queues, or proprietary APIs. Register your module using `caddy.RegisterModule` and reference it in the `admin.config.load` configuration using your module's registered name and any custom parameters your loader requires.

### What happens to existing connections when Caddy reloads its configuration?

Existing connections complete gracefully while Caddy swaps the active context. The **`unsyncedDecodeAndRun`** function provisions the new configuration in the background, then atomically updates `currentCtx` under mutex protection. Old applications stop only after new applications start, ensuring continuous request handling during the transition.