1. The problem I’m having:
We run Caddy in front of Apache on a shared hosting server, with on-demand TLS for several thousand customer domains. The ask endpoint is a small PHP script served by the local Apache on 127.0.0.80:80, which answers 200 for a domain we host and 403 otherwise.
Last night Caddy was restarted unattended by a package update at 04:35. At that same moment our ask endpoint was unavailable (our own fault, and unrelated to Caddy — the backend Apache was mid-restart, so the endpoint was refusing connections).
The result was a full TLS outage, and what surprised us is that it was not limited to new domains. In the two minutes following the restart Caddy logged 1187 failed to get permission for on-demand certificate events across 161 distinct hostnames. We checked every one of those 161 against Caddy’s own storage
Those 155 certificates were valid and had been obtained weeks earlier — for example:
2026-08-02 00:57 certificates/acme-v02.api.letsencrypt.org-directory/www.blue-harbour.ch/www.blue-harbour.ch.crt
2026-08-25 12:22 certificates/acme-v02.api.letsencrypt.org-directory/www.northfield-media.ch/www.northfield-media.ch.crt
2026-07-11 07:24 certificates/acme-v02.api.letsencrypt.org-directory/www.cloudmarten.ch/www.cloudmarten.ch.crt
2026-07-29 07:17 certificates/acme-v02.api.letsencrypt.org-directory/www.airport-rides.com/www.airport-rides.com.crt
So after a restart the certificate cache is cold, and it appears every hostname has to pass the ask check again before its certificate can be loaded from storage — even though no new certificate needs to be issued. With ask unreachable, none of them could be served and the sites were down.
Is there a setting that lets Caddy load and serve an existing on-demand certificate from storage when the ask endpoint is temporarily unavailable, and only consult ask when it would actually need to obtain a new certificate?
If there isn’t, we would like to suggest it: the failure mode is quite severe — a brief blip on the ask backend, coinciding with a restart, takes down every site on the server rather than just blocking new issuance. Serving a certificate we already hold does not seem to require a fresh permission check.
We are aware we can (and will) make our ask endpoint more robust. But we would prefer not to have a hard dependency between “a helper endpoint is up at the exact moment Caddy starts” and “certificates we already own can be served”.
Thanks a lot for any guidance.
2. Error messages and/or full log output:
Caddy restart (systemd):
Aug 31 04:35:27 srv01 systemd[1]: Starting Caddy...
Aug 31 04:35:28 srv01 systemd[1]: Started Caddy.
Representative errors immediately after the restart. Domain names have been replaced with fictional ones; everything else is verbatim:
{"level":"error","ts":1788143728.363686,"logger":"tls","msg":"failed to get permission for on-demand certificate","domain":"www.blue-harbour.ch","error":"checking http://127.0.0.80/ask.php to determine if certificate for hostname 'www.blue-harbour.ch' should be allowed: Get \"http://127.0.0.80/ask.php?domain=www.blue-harbour.ch\": dial tcp 127.0.0.80:80: connect: connection refused"}
{"level":"error","ts":1788143728.5098705,"logger":"tls","msg":"failed to get permission for on-demand certificate","domain":"www.northfield-media.ch","error":"checking http://127.0.0.80/ask.php to determine if certificate for hostname 'www.northfield-media.ch' should be allowed: Get \"http://127.0.0.80/ask.php?domain=www.northfield-media.ch\": dial tcp 127.0.0.80:80: connect: connection refused"}
{"level":"error","ts":1788143728.5297127,"logger":"tls","msg":"failed to get permission for on-demand certificate","domain":"www.cloudmarten.ch","error":"checking http://127.0.0.80/ask.php to determine if certificate for hostname 'www.cloudmarten.ch' should be allowed: Get \"http://127.0.0.80/ask.php?domain=www.cloudmarten.ch\": dial tcp 127.0.0.80:80: connect: connection refused"}
Grouped counts for the same window (04:35–04:36):
1187 tls failed to get permission for on-demand certificate
252 http.log.error dial tcp 127.0.0.80:80: connect: connection refused
Note: our global log level is ERROR, so we do not have debug output for the incident itself.
3. Caddy version:
v2.11.4 h1:XKxkMTgNSizEvKG6QHue6cAsFOteU2qA61w2tKkCWi0=
4. How I installed and ran Caddy:
Self-built RPM package, installed with dnf, run under systemd.
a. System environment:
CloudLinux 9.x (RHEL 9 derivative), x86_64, systemd. No Docker. Caddy sits in front of Apache httpd on the same host; Apache listens on 127.0.0.80:80 and serves both the customer sites and the ask endpoint.
b. Command:
systemctl start caddy
c. Service/unit/compose file:
# caddy.service
#
# For using Caddy with a config file.
#
# Make sure the ExecStart and ExecReload commands are correct
# for your installation.
#
# See https://caddyserver.com/docs/install for instructions.
#
# WARNING: This service does not use the --resume flag, so if you
# use the API to make changes, they will be overwritten by the
# Caddyfile next time the service is restarted. If you intend to
# use Caddy's API to configure it, add the --resume flag to the
# `caddy run` command or use the caddy-api.service file instead.
[Unit]
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target
StartLimitIntervalSec=0
[Service]
Type=notify
User=caddy
Group=caddy
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile --force
TimeoutStopSec=5s
LimitNOFILE=1048576
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
Restart=always
RestartSec=3
ReadWritePaths=/etc/pki
Environment=GOMEMLIMIT=4096MiB
[Install]
WantedBy=multi-user.target
d. My complete Caddy config:
{
admin 127.0.0.1:8888
default_bind 127.0.0.1 [::1] 203.0.113.10 [2001:db8::10] 203.0.113.11 [2001:db8::11]
grace_period 3s
log {
output file /var/log/caddy/caddy.log {
roll_size 250MiB
roll_keep_for 15d
}
level ERROR
}
email admin@example-host.net
on_demand_tls {
ask http://127.0.0.80/ask.php
}
default_sni srv01.example-host.net
servers {
protocols h1 h2
trusted_proxies combine {
cloudflare {
interval 12h
timeout 15s
}
static 203.0.113.20 203.0.113.21 203.0.113.22
}
}
import local.d/*.conf
}
# Common options applied to every customer site
(common) {
reverse_proxy http://127.0.0.80:80 {
header_up X-Forwarded-Proto {http.request.scheme}
header_up X-Forwarded-Port {http.request.local.port}
}
}
http://*.*.*.*,
http://*.*.*.*.*,
http://*.*.*.*.*.* {
import common
}
# Workaround to match only host names and not ip-addresses:
https://*.*,
https://*.*.*,
https://*.*.*.*,
https://*.*.*.*.*,
https://*.*.*.*.*.* {
import common
tls {
on_demand
load /etc/caddy/certs
}
}
5. Links to relevant resources:
Not relevant