# How Caddy's JSON API for Dynamic Configuration Works: Complete Technical Guide

> Master Caddy's JSON API for dynamic configuration. Learn how to modify and reload Caddy's settings in real-time via REST endpoints without restarting the server. A complete technical guide.

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

---

**Caddy's JSON API enables runtime configuration changes through an admin HTTP server that exposes REST endpoints for reading, patching, and reloading the active configuration without process restarts.**

The Caddy web server supports dynamic configuration management through a built-in JSON API, allowing operators to modify TLS certificates, routes, and server blocks while the server remains active. This capability centers on the admin server configured via the `Admin` field in the top-level `Config` struct, which exposes HTTP endpoints for granular configuration manipulation. According to the caddyserver/caddy source code, the API implements optimistic concurrency control using ETags and atomic configuration swaps to ensure safe, concurrent updates.

## Admin Server Architecture and Request Routing

When Caddy starts, the admin server initializes an `adminHandler` (see [`admin.go`](https://github.com/caddyserver/caddy/blob/main/admin.go) lines 221-236) that registers routes mapping to specific configuration operations. The handler receives all requests via `adminHandler.ServeHTTP` (lines 787-795) and dispatches them through an internal mux after performing security checks via `checkHost` and `checkOrigin`.

The API exposes the following primary routes:

- **`/config/*`** – Managed by `handleConfig` for read and write operations on configuration subtrees
- **`/load`** – Handled by `adminLoad.handleLoad` for full configuration replacement
- **`/id/*`** – Resolved by `handleConfigID` for object lookup using the `@id` meta-field
- **`/stop`** – Processed by `handleStop` for graceful server termination

Read requests execute under a read lock, while mutations acquire a write lock through `rawCfgMu` to protect the internal `rawCfg` map (see the concurrency comments at [`admin.go`](https://github.com/caddyserver/caddy/blob/main/admin.go) lines 45-47).

## Reading Configuration with ETag Support

The `GET /config/...` endpoint triggers `handleConfig` (lines 1011-1032), which retrieves JSON subtrees through `readConfig` (lines 267-273 in [`caddy.go`](https://github.com/caddyserver/caddy/blob/main/caddy.go)). This function wraps `unsyncedConfigAccess` to traverse the raw configuration structure and return the requested data.

For optimistic concurrency control, the handler generates an ETag header using `makeEtag` (lines 997-1002). The ETag value follows the format `"path <hex-hash>"`, where the hash represents the current state of the requested subtree. Clients must include this value in subsequent `If-Match` headers when modifying configuration to prevent lost updates.

```bash
curl -s http://localhost:2019/config/ | jq .

```

The response includes an `Etag` header (e.g., `Etag: "config 7b8e...f0"`) that represents the configuration's current hash state.

## Mutating Configuration Dynamically

Configuration changes occur through `POST`, `PUT`, `PATCH`, and `DELETE` requests to `/config/*`, which delegate to `changeConfig` (lines 145-164 in [`caddy.go`](https://github.com/caddyserver/caddy/blob/main/caddy.go)). This function implements a multi-stage workflow for safe configuration updates.

### The changeConfig Workflow

The mutation process follows these steps:

1. **If-Match Validation** – Parses the quoted header and computes the current hash via `unsyncedConfigAccess`, rejecting requests with HTTP 412 if the hash differs (lines 172-203)
2. **Apply Change** – Calls `unsyncedConfigAccess` with the HTTP method, JSON body, and target path to traverse `rawCfg` and perform the operation, including array handling and ellipsis support (lines 441-495 and 511-595 in [`admin.go`](https://github.com/caddyserver/caddy/blob/main/admin.go))
3. **Detect No-Op** – Compares the marshalled representation (`newCfg`) against the cached copy (`rawCfgJSON`) and returns `errSameConfig` if unchanged and `forceReload` is false (lines 220-225)
4. **Re-index @id Fields** – Rebuilds the lookup table via `indexConfigObjects` (lines 274-282) to maintain the `/id/*` resolution capability
5. **Load New Config** – Invokes `unsyncedDecodeAndRun` (lines 312-324) to strip meta-fields, unmarshal into `*Config`, and re-initialize plugins and listeners atomically

If any step fails, Caddy executes rollback logic (lines 336-353) to restore the previous configuration, ensuring the running server remains stable.

### Array Manipulation with Ellipsis Syntax

To append elements to arrays without overwriting existing items, use the trailing `...` path syntax:

```bash
curl -X POST \
     -H "Content-Type: application/json" \
     -d '[{"handle":[{"handler":"static_response","body":"new"}]}]' \
     http://localhost:2019/config/apps/http/servers/srv0/routes/...

```

The trailing ellipsis instructs Caddy to treat the payload as an array and append each element to the existing routes rather than replacing the entire array.

## Full Configuration Replacement

The `/load` endpoint provides atomic full-configuration replacement through `handleLoad` in the `adminLoad` module. This endpoint reads the request body, optionally validates an `If-Match` header, and calls `caddy.Load` (lines 68-78 in [`caddyconfig/load.go`](https://github.com/caddyserver/caddy/blob/main/caddyconfig/load.go)). This function clears the previous configuration and runs the new one exactly like a fresh `caddy run` command, but without restarting the process.

```bash
cat new-config.json | curl -X POST \
    -H "Content-Type: application/json" \
    http://localhost:2019/load

```

If the new configuration fails to load, Caddy retains the previous active configuration, preventing service disruption.

## Object Resolution Using @id

When configuration objects include the `@id` meta-field, `indexConfigObjects` (lines 274-300 in [`caddy.go`](https://github.com/caddyserver/caddy/blob/main/caddy.go)) maintains a mapping from ID values to their JSON pointer paths. The `GET /id/<id>` endpoint uses this index to retrieve objects without requiring knowledge of their full path structure.

```bash
curl http://localhost:2019/id/myroute

```

This resolves the object tagged with `"@id":"myroute"` regardless of its nested location within the configuration tree.

## Practical API Usage Examples

### Optimistic Concurrency with ETags

To safely update a configuration field while preventing conflicts from concurrent modifications:

```bash

# Capture the current ETag

etag=$(curl -i -s http://localhost:2019/config/ | awk -F'"' '/Etag/ {print $2}')

# Submit patch with validation

curl -X PATCH \
     -H "Content-Type: application/json" \
     -H "If-Match: \"$etag\"" \
     -d '{"listen":"0.0.0.0:2020"}' \
     http://localhost:2019/config/admin

```

If the configuration changed between the GET and PATCH requests, Caddy returns HTTP 412 Precondition Failed, prompting the client to retry with the current state.

### Deleting Configuration Subtrees

Remove specific configuration paths using DELETE requests:

```bash
curl -X DELETE http://localhost:2019/config/apps/http/servers/srv0/routes/0

```

This removes the first route from the specified server's routes array.

## Summary

- The **admin server** initializes at startup via `AdminConfig`, creating an `adminHandler` that routes requests to specific handlers like `handleConfig` and `handleLoad`
- **ETag-based optimistic concurrency** prevents lost updates by requiring matching hashes in `If-Match` headers for all mutation operations
- The **`changeConfig` function** in [`caddy.go`](https://github.com/caddyserver/caddy/blob/main/caddy.go) orchestrates safe configuration updates through validation, application, re-indexing, and atomic loading stages
- **Automatic rollback** occurs if configuration loading fails, ensuring the server never enters an undefined state
- The **`@id` meta-field** system enables stable object references via the `/id/*` endpoint, decoupling clients from specific JSON paths

## Frequently Asked Questions

### What port does Caddy's JSON API use by default?

By default, the admin server listens on `localhost:2019`. You can modify this through the `admin` section of your Caddy configuration or via the `CADDY_ADMIN` environment variable. The `listen` field accepts network addresses in the format `host:port` or Unix socket paths.

### How does Caddy handle concurrent configuration updates from multiple clients?

Caddy implements **optimistic concurrency control** using ETag headers. When reading configuration, the server returns an ETag representing the current state hash. Clients must include this value in the `If-Match` header when submitting changes. If the configuration changed between read and write (hash mismatch), Caddy returns HTTP 412 Precondition Failed, forcing the client to fetch the current state and retry.

### What happens if a dynamic configuration update fails halfway through?

Caddy maintains the previous configuration in memory during the update process. If `unsyncedDecodeAndRun` fails to load the new configuration at [`caddy.go`](https://github.com/caddyserver/caddy/blob/main/caddy.go) lines 336-353, the server automatically rolls back to the previous stable configuration. This ensures that partial or invalid configurations never disrupt active services.

### Can I use the JSON API to update TLS certificates dynamically?

Yes. You can POST new certificate data to `/config/apps/tls/certificates` or use the `/id/*` endpoint if your certificates have `@id` tags. Caddy will reload the TLS configuration without dropping existing connections, enabling zero-downtime certificate rotation through the API.