caddyserver/caddy · error

loading dynamic config from %T: %v

Error message

loading dynamic config from %T: %v

What it means

Thrown when the dynamic config loader module implements ConfigLoader and its LoadConfig(ctx) returns an error in the synchronous path (no LoadDelay configured). The message includes %T, the concrete loader module type, so you can identify which loader failed.

Source

Thrown at caddy.go:670

						err = runLoadedConfig(loadedConfig)
						if errors.Is(err, errSameConfig) {
							logger.Info("dynamically-loaded config was unchanged; will retry")
							continue
						}
					case <-ctx.Done():
						if !timer.Stop() {
							<-timer.C
						}
						logger.Info("stopping dynamic config loading")
					}
					break
				}
			}()
		} else {
			// if no LoadDelay is provided, will load config synchronously
			loadedConfig, err := val.(ConfigLoader).LoadConfig(ctx)
			if err != nil {
				return fmt.Errorf("loading dynamic config from %T: %v", val, err)
			}
			// do this in a goroutine so current config can finish being loaded; otherwise deadlock
			go func() { _ = runLoadedConfig(loadedConfig) }()
		}
	}

	return nil
}

// ConfigLoader is a type that can load a Caddy config. If
// the return value is non-nil, it must be valid Caddy JSON;
// if nil or with non-nil error, it is considered to be a
// no-op load and may be retried later.
type ConfigLoader interface {
	LoadConfig(Context) ([]byte, error)
}

// Stop stops running the current configuration.

View on GitHub (pinned to 50e54ee279)

Solutions

  1. Check which loader type appears in the message and inspect that module's LoadConfig error semantics
  2. For the http loader: verify the URL, network reachability, TLS trust, and that the body is valid Caddy JSON
  3. Set a load_delay so loading happens asynchronously and a transient fetch failure does not abort startup
  4. Test the remote config payload with `caddy validate --adapter json --config <file>` or `curl` first

Example fix

// before
{"admin": {"config": {"load": {"http": {"url": "http://cfg.internal/current"}}}}}
// after (retry-friendly async load)
{"admin": {"config": {"load_delay": "5s", "load": {"http": {"url": "http://cfg.internal/current"}}}}}
Defensive patterns

Strategy: fallback

Validate before calling

// for the http loader: health-check the config endpoint before Caddy starts
resp, err := http.Get(cfgURL)
if err != nil || resp.StatusCode != 200 {
    log.Fatal("remote config unavailable")
}
body, _ := io.ReadAll(resp.Body)
if err := json.Valid(body); ! err { log.Fatal("remote config is not valid JSON") }

Try / catch

if err := runLoad(); err != nil {
    if strings.Contains(err.Error(), "loading dynamic config from") {
        // fall back to last-known-good config file instead of aborting
    }
}

Prevention

When it happens

Trigger: admin.config.load is set, LoadDelay is 0/unset, and val.(ConfigLoader).LoadConfig(ctx) returns a non-nil error — e.g. the HTTP loader got a non-200 response, or a custom loader could not fetch/decode its payload.

Common situations: Config pulled from a remote URL at startup where the endpoint is down, returns invalid Caddy JSON, or requires auth that was not configured. Because it fires synchronously, Caddy startup aborts.

Related errors


AI-assisted analysis of caddyserver/caddy@50e54ee279 (2026-08-15). Data as JSON: /api/errors/f0a7b6007c628c3a. Report an issue: GitHub.