# Caddy on-demand TLS vs a managed service for customer domains

Canonical source: https://approximated.app/blog/caddy-on-demand-tls-vs-managed-custom-domains/
Author: Approximated
Published: 2026-10-26
Last reviewed: 2026-10-26

<a id="short-answer"></a>

**Short answer:** Caddy's on-demand TLS is a good, production-capable way to give customer custom domains automatic HTTPS. It gets a certificate during the first TLS handshake for a hostname your `ask` endpoint approves. The certificate is only part of the job. You also run the permission endpoint, shared certificate storage, CA rate-limit budgets, monitoring, abuse handling and routing. If your team wants to own that edge, Caddy is an excellent base. If it doesn't, a managed custom domain service moves that work off your plate.

<a id="how-it-works"></a>

## How Caddy on-demand TLS works

Normally Caddy gets certificates for the names in its config when the config loads. On-demand TLS waits. As [Caddy's docs](https://caddyserver.com/docs/automatic-https) explain, it gets a certificate during the first TLS handshake that needs one, instead of at config load. Caddy recommends it for cases where you aren't in control of the domain names, such as customer domains. That describes a SaaS custom domain feature exactly.

The flow for a new customer hostname looks like this:

1. A visitor's browser opens a TLS connection and sends the hostname in the handshake.
2. Caddy has no certificate for that name, so it asks your permission endpoint whether the name is allowed. A 2xx response allows it. Any other status cancels issuance and fails the handshake.
3. Caddy holds the handshake while it completes an ACME order with a certificate authority. The docs say the delay "is usually only a few seconds", and only that first handshake is slow.
4. Caddy writes the certificate to its storage, serves it, and renews it in the background from then on.

The Caddyfile for that setup is short. The [global `on_demand_tls` option](https://caddyserver.com/docs/caddyfile/options) points at your endpoint, and a catch-all HTTPS site turns on on-demand issuance:

```plaintext
{
	email you@example.com
	on_demand_tls {
		ask http://localhost:9123/allowed-domain
	}
}

https:// {
	tls {
		on_demand
	}
	reverse_proxy localhost:8080
}
```

In current Caddy releases, the Caddyfile `ask` setting is a shortcut for the `http` permission module. In the JSON config, the old `ask` field is marked deprecated in favor of `permission` ([Caddy v2.11.4 source](https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddytls/ondemand.go)). Both send the same question to the same kind of endpoint.

<a id="why-caddy"></a>

## Why Caddy is a strong do-it-yourself choice

Caddy does a lot of the hard certificate work for you. It speaks ACME, stores and renews certificates, retries failed orders, and coordinates instances that share storage. By default it uses Let's Encrypt, and it adds ZeroSSL as a fallback issuer when an account email is configured. Caddy describes itself as "the first (and so far only) server" with fully redundant automatic CA failover. Its author has written a [wiki guide to serving tens of thousands of domains](https://caddy.community/t/serving-tens-of-thousands-of-domains-over-https-with-caddy/11179), which treats on-demand TLS as a first-class use case.

For a small team with one region and a modest number of customer domains, a single Caddy server with a well-built permission endpoint can be the whole edge. The rest of this post covers what grows around that server once real customers depend on it.

<a id="ask-endpoint"></a>

## The ask endpoint is production infrastructure

Caddy is direct about this. The automatic HTTPS docs say on-demand TLS "must be both enabled and restricted to prevent abuse." The global options page calls the `on_demand_tls` option "required for use in production environments." Current Caddy refuses to enable on-demand TLS without a permission module, because otherwise any server name a client sends could trigger issuance. Anyone could point domains at your server and make you request certificates for them.

A good endpoint checks one thing: does this exact, normalized hostname belong to an active customer? Caddy's docs ask for an answer in "a few milliseconds, ideally", and advise against DNS queries or other network calls inside it. So check your own domain table, not the customer's DNS. Keep the endpoint private. A comment in Caddy's source notes that exposing it publicly would leak which domains your application serves.

The endpoint also matters after issuance. In current Caddy, the permission check decides whether a certificate may be _loaded from storage_ as well as obtained from a CA ([ondemand.go](https://github.com/caddyserver/caddy/blob/v2.11.4/modules/caddytls/ondemand.go)). One user on the Caddy forum [reported a full TLS outage](https://caddy.community/t/caddy-with-on-demand-tls-total-outage-after-an-unattended-restart-when-ask-endpoint-is-down/33898) after an unattended restart while their ask endpoint was down. In a reply dated September 2, 2026, Caddy's author confirmed this works as intended and wrote that "the ask endpoint must not fail". He recommends a local, lightweight, reliable endpoint on the same machine. The practical lesson is to give the endpoint the same availability as the proxy itself. If it runs inside your main application, a restart of the app or its database can stop the proxy from serving certificates it already has.

<a id="storage"></a>

## Shared storage once you run more than one instance

With the default setup, Caddy keeps certificates in a folder under `$HOME`, which must be writable and persistent. That is fine for one server. Once you add a second server for redundancy or capacity, every instance should use the same storage. [Caddy's storage docs](https://caddyserver.com/docs/automatic-https) say instances configured with the same storage share certificates automatically and coordinate certificate management as a cluster.

In practice, that means choosing a storage module or shared filesystem and operating it as critical infrastructure. If the storage is lost, every certificate has to be issued again. Let's Encrypt's [integration guide](https://letsencrypt.org/docs/integration-guide/) warns that containers which issue at startup without durable storage are likely to hit rate limits, and that a site can be broken for several days until the limit expires.

<a id="issuers-and-limits"></a>

## Issuer fallback and CA rate limits

Issuer fallback helps when one CA is down or refusing an order. Check each CA's account requirements before you rely on it. ZeroSSL's [ACME documentation](https://zerossl.com/documentation/acme/) requires EAB credentials and publishes fair-use terms rather than numeric limits. Let's Encrypt publishes its [rate limits](https://letsencrypt.org/docs/rate-limits/). These are the ones a SaaS with customer domains is most likely to meet (page last updated August 5, 2026):

| Limit | Allowance | When it bites |
| --- | --- | --- |
| New Orders per Account | 300 orders per 3 hours, refilling 1 every 36 seconds | A burst of new customer domains, such as an import or a launch. |
| New Certificates per Registered Domain | 50 certificates per 7 days, across all accounts | Many tenants under one registered domain, for example many hostnames under one customer's domain. |
| Authorization Failures per Identifier per Account | 5 failures per hour | A customer whose DNS doesn't point at you yet, retried too often. |
| Consecutive Authorization Failures per Identifier per Account | 1,152 in a row, refilling 1 per day | A hostname that keeps failing. Depending on retry rate, the pause arrives after about 10 days to about 4 months. Issuance for it is paused until someone unpauses it. |

Renewals are treated more kindly. Renewals made through ARI, the ACME Renewal Information protocol, are exempt from all of these limits, under the conditions on that page. Let's Encrypt also accepts override requests for the first two limits, but notes that requests take a few weeks to process. Lifetimes are getting shorter too. Let's Encrypt's default certificates move from 90 to [64 days on February 10, 2027](https://letsencrypt.org/2026/10/07/64-day-certs), and to 45 days on February 16, 2028. Caddy renews automatically, but your storage and CAs will see more renewal traffic.

Caddy itself also spaces out its requests. Its docs list an internal limit of 10 attempts per ACME account per 10 seconds. After all issuers fail, it backs off exponentially, up to one day between attempts, for up to 30 days.

<a id="first-request"></a>

## First-request latency and failed issuance

The first visitor to a new hostname waits for issuance. Caddy's docs warn that a CA taking more than a few seconds will hurt that first visitor's experience. In practice, you can trigger that first handshake yourself after a customer's DNS is correct, so a real visitor doesn't pay the wait.

The harder case is a hostname that keeps failing, usually because DNS points somewhere else. Each attempt can count against the authorization failure limits above. Two other documented details matter here. Caddy aborts in-flight tasks, including ACME transactions, when the config changes, and its docs suggest batching config changes. Retries can also continue in the background for up to 30 days, so a domain your customer abandoned can keep generating work unless your permission endpoint starts refusing it.

<a id="monitoring"></a>

## Monitor certificate state yourself

Caddy renews certificates on its own, but nothing in it tells your customer or your support team that a domain is stuck. Let's Encrypt [stopped sending expiration emails on June 4, 2025](https://letsencrypt.org/2025/01/22/ending-expiration-emails). If you run issuance yourself, you need your own checks. At minimum, track for each customer hostname: whether DNS points at you, whether a valid certificate is being served, when it expires, and whether a real HTTPS request reaches the right tenant. Then surface those states in your product so customers can fix their own DNS. Our [onboarding checklist](https://approximated.app/blog/custom-domain-onboarding-checklist/) describes those states.

<a id="abuse"></a>

## Unknown hostnames, abuse and DDoS

A public proxy that accepts any hostname is a target. Caddy's wiki lists the risks plainly: clients are untrusted and can send any server name, CAs can rate limit failed attempts, and attackers can point many domains at your server. It warns that an endpoint which approves too much can get you blocked by CAs as an abuse vector.

The permission endpoint stops most of that, but it becomes a target itself. A flood of handshakes with random hostnames turns into a flood of permission checks. Cache answers, put a budget on database lookups, and make sure an attack can't lock out a real customer's domain. Then think about the traffic that does reach a valid hostname. Caddy is a web server, not a WAF or DDoS service, so request filtering, bot handling and volumetric protection are separate systems you add and run. Our [custom domain security guide](https://approximated.app/blog/custom-domain-security/) covers the tenant-side controls that sit next to them.

<a id="routing"></a>

## Apex domains, IPv6 and global routing

Customers who want their root domain, such as `example.com`, usually can't use a CNAME there. They need an A record to an address you control, or a provider's ALIAS feature. That makes your IP address a long-term promise, because changing it means asking every apex customer to update DNS. Our [apex domains guide](https://approximated.app/blog/apex-domains-for-saas/) covers the options.

If you also publish an IPv6 address, test validation and HTTPS over both address families. A customer can add an AAAA record you don't serve correctly, and visitors on that path will fail. For a global audience, one server in one region adds latency far from that region. Running Caddy in several regions means shared storage that every region can reach, plus anycast addressing or location-aware DNS in front.

<a id="decision"></a>

## When DIY Caddy is enough, and when a managed service is simpler

Caddy on-demand TLS is usually enough when:

- Your team already runs servers, storage and on-call, and wants direct control of the edge.
- You serve customers from one region, or you're ready to run shared storage across regions.
- Your customer-domain count and signup rate stay well inside the CA limits above.
- You can host a fast, highly available permission endpoint close to the proxy.
- You have, or will build, monitoring and customer-facing domain status.

A managed service is usually simpler when:

- Custom domains are an important feature, but edge infrastructure isn't your product.
- You need high availability, several regions, or apex support on stable IP addresses without building them.
- You want WAF and DDoS protection in front of customer domains without running it yourself.
- You want domain and certificate status as an API and webhooks rather than logs.
- Nobody on the team wants to be paged when a CA, storage cluster or permission endpoint has a bad night.

Count the work honestly in both directions. A managed service adds a vendor and a monthly bill. DIY adds engineering time that rarely shows up in the first estimate. Our [DIY comparison](https://approximated.app/approximated-diy-comparison/) lays out the components side by side.

<a id="approximated"></a>

## Where Approximated fits

Approximated is a managed option. We issue and renew certificates automatically, but only for hostnames configured as virtual hosts. A name with no virtual host is refused before any certificate authority is contacted. We work with multiple certificate authorities, so if one can't issue a certificate, issuance falls back to another.

Around that, we give each cloud cluster a dedicated anycast IPv4 address for apex A records, monitor DNS, SSL and proxy-hit status for configured domains, and send [webhooks](https://approximated.app/docs/#webhooks) when that status changes. Our [WAF and DDoS protection](https://approximated.app/#waf) runs in the same cluster. If you later need traffic and certificates on your own infrastructure, our [self-hosted plans](https://approximated.app/self-hosted/) keep that path open. Prices are on our [pricing page](https://approximated.app/pricing/).

<a id="faq"></a>

## Frequently asked questions

<a id="faq-production-ready"></a>

### Is Caddy on-demand TLS good enough for production custom domains?

<a id="faq-production-ready-answer"></a>

Yes, when you run it carefully. Caddy handles ACME, storage, renewal, retries and CA fallback well. Production use also needs a fast and highly available permission endpoint, durable shared storage for more than one instance, attention to CA rate limits, certificate monitoring, and abuse protection. Those parts decide whether it is good enough for you.

<a id="faq-multi-tenant"></a>

### How do I use Caddy on-demand TLS for a multi-tenant SaaS?

<a id="faq-multi-tenant-answer"></a>

Enable on_demand inside a catch-all HTTPS site, point the global on_demand_tls ask setting at an internal endpoint, and have that endpoint return 200 only for hostnames that belong to an active tenant. Proxy requests to your app, and let the app pick the tenant from the trusted Host header. Add shared storage before you run a second Caddy instance.

<a id="faq-ask-check"></a>

### What should a Caddy ask endpoint check?

<a id="faq-ask-check-answer"></a>

It should check whether the exact, normalized hostname belongs to an active customer, and nothing slower. Caddy recommends answering in a few milliseconds and avoiding DNS queries or other network calls. Read from a local table or cache, keep the endpoint off the public internet, and return a non-2xx status for anything unknown.

<a id="faq-ask-down"></a>

### What happens to existing certificates if the ask endpoint goes down?

<a id="faq-ask-down-answer"></a>

In current Caddy, the permission check also controls loading certificates from storage, not only issuing new ones. A forum user reported a full TLS outage after a restart while their endpoint was down, and Caddy's author confirmed that behavior is intended. Run the endpoint locally and keep it as reliable as the proxy itself.

<a id="faq-managed"></a>

### When should I pay for a managed custom domain service instead of running Caddy?

<a id="faq-managed-answer"></a>

Pay for a service when the work around certificates costs more than the bill. That usually means you need several regions, stable apex IP addresses, WAF and DDoS protection, domain status APIs and webhooks, or on-call coverage you don't want to staff. If your team already runs that kind of infrastructure, DIY Caddy can be the better fit.
