ACME challenge: HTTP-01 vs DNS-01, and how to fix a failed _acme-challenge

An ACME challenge is the proof a certificate authority (CA) such as Let's Encrypt asks for before it issues a certificate. The ACME protocol hands your client a token. Your client has to show that it controls the domain by publishing that token where the CA can read it. The three challenge types are HTTP-01, DNS-01 and TLS-ALPN-01.
If your challenge failed, skip to the debugging checklist. If you issue certificates for domains your customers own, jump to the multi-tenant section. That's where most guides stop being useful, because you can't edit your customer's DNS zone.
What is an ACME challenge?
ACME is the protocol CAs use to automate certificate issuance. The challenge is the "prove it" step. No proof, no certificate.
Here are the three types side by side:
| HTTP-01 | DNS-01 | TLS-ALPN-01 | |
|---|---|---|---|
| What you prove | You control the web server for the hostname | You control the DNS zone for the hostname | You control the TLS listener on port 443 |
| Where the proof lives | http://<hostname>/.well-known/acme-challenge/<token> | TXT record at _acme-challenge.<hostname> | A special certificate served over TLS |
| Wildcards | No | Yes | No |
| What usually breaks | Port 80 blocked, redirects, CDN or firewall in the way | Wrong record name, stale values, propagation lag, wrong zone | Proxies or load balancers that terminate TLS before your client sees it |
Most people pick between HTTP-01 and DNS-01. TLS-ALPN-01 exists, but you will meet it rarely.
How the DNS-01 challenge works, step by step
The _acme-challenge TXT record glossary entry is the short version. Here is the longer one.
- Your ACME client asks the CA for a certificate for
app.example.com. - The CA returns a token. The client derives a value from it.
- The client publishes that value as a TXT record at
_acme-challenge.app.example.com. - The CA queries DNS for that record. If the value matches, validation passes.
- The CA issues the certificate. The client removes the TXT record.
In flow form:
ACME client CA (Let's Encrypt) DNS
| 1. order cert for host | |
|----------------------------->| |
| 2. token | |
|<-----------------------------| |
| 3. publish TXT at _acme-challenge.host |
|-------------------------------------------------------->|
| | 4. query TXT |
| |------------------------->|
| | 5. value matches |
| |<-------------------------|
| 6. certificate issued | |
|<-----------------------------| |
| 7. delete TXT record |
|-------------------------------------------------------->|
The record looks like this:
_acme-challenge.app.example.com. 300 IN TXT "gfj9Xq...Rg85nM"
Read it with dig:
dig TXT _acme-challenge.app.example.com +short
The record is temporary. After validation it has done its job, so an empty answer after a successful issuance is normal. An empty answer during a failed one is your first clue.
HTTP-01 vs DNS-01: which should you use?
Short version: use HTTP-01 when you have a public web server on port 80. Use DNS-01 when you don't, or when you need a wildcard.
| Your situation | Better fit |
|---|---|
| Public web server reachable on port 80 | HTTP-01 |
| No public server (internal service, mail server, VPN) | DNS-01 |
You need a wildcard certificate (*.example.com) | DNS-01 (the only option) |
| You have an API for your DNS provider | DNS-01 |
| No DNS API access, but you control the web server | HTTP-01 |
Behind a CDN or firewall that blocks or rewrites /.well-known/ | DNS-01 |
HTTP-01 is the boring, correct choice for a single public site. It needs no DNS credentials, and there is little to leak. DNS-01 is the boring, correct choice for wildcards and for hosts the internet can't reach.
One note so nobody misreads this as a feature claim: wildcard certificates require DNS-01. Domainee does not support wildcard domains at the moment.
Why an ACME challenge fails (and how to check each cause)
When an ACME challenge fails, the cause is almost always one of six things. Check them in this order.
The free checks below need no API key. They are limited to 30 requests per minute and 500 per day per IP. Browser versions are listed at /free-tools.
1. The TXT record is missing or on the wrong name
The record must live at _acme-challenge.<hostname>. Not at the hostname itself. Not at _acme-challenge.example.com when you are validating app.example.com. Not with the zone name doubled up (_acme-challenge.app.example.com.example.com), which some DNS panels do when you type a full name.
Check: run the free TXT record lookup on _acme-challenge.<hostname>. Or use dig:
dig TXT _acme-challenge.app.example.com +short
No answer means the CA sees nothing either.
2. Stale or duplicate TXT values
Old values from earlier attempts can pile up. Some clients and some DNS panels mishandle multiple values at one name.
Check: look at the full TXT answer. If you see several values, delete the old ones and retry. Keep only the value from the current order.
3. DNS propagation lag
The record exists at your provider, but the CA's resolvers haven't seen it yet. Caching works against you here. If a resolver recently got an empty answer for that name, it may hold on to it until the negative-caching TTL expires.
Check: run the DNS propagation checker. If some locations show the value and others don't, wait. A good rule is the record's TTL plus the zone's negative-caching TTL. Then retry once. Retrying every ten seconds just burns CA rate limits.
4. Wrong zone or delegated nameservers
You edited a zone that isn't authoritative. Maybe the domain's nameservers point to a different provider than the one you're logged into. Maybe a subdomain is delegated elsewhere. It happens a lot after a migration.
Check: look up the NS records for the domain and for the subdomain:
dig NS example.com +short
dig NS app.example.com +short
If app.example.com returns its own NS records, that's where the TXT record has to go.
5. A CAA record blocks the CA
This is the sneaky one. A CAA record tells CAs which of them may issue for a domain. If CAA records exist and none lists Let's Encrypt, issuance is refused. The challenge itself can pass and the order still fails.
Check:
dig CAA example.com +short
If the answer lists CAs and Let's Encrypt isn't among them, add it:
example.com. IN CAA 0 issue "letsencrypt.org"
If you also use ZeroSSL, list it too. Domainee's docs mention ZeroSSL in their CAA guidance. CAA is inherited down the tree. A record on example.com applies to app.example.com unless the subdomain has its own.
Domainee's preflight warns on CAA records that block Let's Encrypt, so you hear about this before the certificate fails.
6. CA rate limits
Let's Encrypt limits repeated failed validations and duplicate certificates. If you retry in a tight loop, you can lock yourself out for a while. Fix the cause first. Then retry. While you debug, use the CA's staging environment so you don't spend production limits.
The 2am version
The worst failure isn't the first issuance. It's the renewal that quietly stopped working. The DNS API token expired. Someone deleted a CNAME. A firewall rule started eating port 80. Nothing shows until the certificate lapses, and then your pager does the talking.
To see what the CA sees right now, run the hostname through the SSL certificate checker in the free tools. Look at the expiry date. Better yet, monitor it automatically.
Delegating _acme-challenge with a CNAME
Here is the pattern. Instead of letting your ACME client write TXT records into the main zone, you add one permanent CNAME:
_acme-challenge.example.com. IN CNAME _acme-challenge.validation.yourzone.net.
When the CA looks up the TXT at _acme-challenge.example.com, DNS follows the CNAME to a zone you control. Your client writes the TXT record there. The CA follows the same path to validate.
Pros:
- No DNS credentials for the main zone sit on your issuing machine.
- You can lock the main zone down and still renew automatically.
- You set the CNAME once. Renewals don't touch the main zone.
Cons:
- You must keep the delegated zone alive. If it vanishes, renewals fail.
- Someone has to add the CNAME to the main zone first, so it only works when you can ask the zone owner.
- It's one more moving part to debug. A typo in the CNAME target looks just like a missing TXT record.
If validation fails with delegation in place, look up the TXT at the original name. The lookup follows the CNAME, so you'll see what the CA sees.
ACME challenges when your customers own the domain
Everything above assumes you can edit the zone, run the web server, or at least ask someone who can. A SaaS with custom domains has none of that. The domain is docs.customer.com. The DNS zone lives at a registrar you have never heard of, and you have no login there.
You can't write to that zone. Asking customers to add TXT records at _acme-challenge is a support-ticket factory. Each ticket is a slightly different mistake from the six above.
Building the full stack yourself drags in ACME rate limits, edge TLS termination, SNI routing, DNS health checks and certificate rotation. A weekend feature turns into a quarter.
Domainee, a custom-domains-as-a-service API from Common Ninja (Domainee by Common Ninja), takes that work. Your customer's job is one CNAME. Where an apex provider disallows CNAME, we offer two anycast A records instead. Apex domains and subdomains both work.
Certificates come from Let's Encrypt. They are issued on first request (on-demand TLS) and renewed automatically. Learn more about automatic SSL for custom domains.
One API call
curl -X POST https://api.domainee.dev/v1/domains \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"hostname": "docs.customer.com",
"originUrl": "https://your-app.example.com"
}'
The response gives you the CNAME your customer adds. The target is edge.domainee.dev:
docs.customer.com. IN CNAME edge.domainee.dev.
That's the whole customer-side change. The target is visible in DNS, since the record shows the Domainee name.
Helping customers get the record in
- Provider-specific steps.
GET /v1/domains/{id}/instructionsdetects the DNS provider from public NS records and returns steps written for that provider. - One-click setup. Domain Connect works on Cloudflare.
- Scoped token. Cloudflare and DigitalOcean customers can supply a scoped API token via
POST /v1/domains/{id}/dns/apply. We use it once and never store it.
Your customer never needs a registrar account for any of this, only a way to add one record. The free tier covers up to 20 domains and 100 GB a month, forever.
Reading certificate state through the API
Each domain is re-checked every 5 minutes. To check right now, call POST /v1/domains/{id}/check. The domain reports a monitorStatus. The state names are Domainee's. The "where to start" column is our debugging suggestion, not a documented cause map, so confirm with the checklist above.
| Monitor state | Where to start |
|---|---|
unknown | Force a check with POST /v1/domains/{id}/check, then read the state again |
dns_not_resolving | Run the DNS propagation checker and confirm the customer added the record |
dns_incorrect | Compare the record your customer added against the CNAME target the API returned |
pending_ssl | Wait for the next check, then read the state again. Certificates are issued on first request |
active_ssl | Nothing. Go have lunch |
ssl_failed | Work through the checklist above, starting with CAA records. Domainee's preflight warns on CAA records that block Let's Encrypt |
ssl_expired | Re-check DNS and CAA first, then force a check |
You don't have to poll. Webhooks fire on domain.verified, domain.failed, domain.expired and domain.monitor_updated. Each request is signed with HMAC-SHA256 in the x-domainee-signature header, so verify it before you trust it.
Wire domain.failed and domain.expired to your alerting and the 2am version of this problem becomes a Slack message at 10am.
FAQ
What is an ACME challenge?
An ACME challenge is the proof a certificate authority such as Let's Encrypt requires before issuing a certificate. Your ACME client shows it controls the domain by publishing a token, either over HTTP, in a DNS TXT record, or over TLS. No proof, no certificate.
What is the _acme-challenge TXT record?
It's a temporary DNS TXT record at _acme-challenge.<hostname> that holds the value derived from the CA's token. The CA queries it during DNS-01 validation. Your client normally deletes it once validation finishes, so an empty answer afterward is normal.
What is the difference between HTTP-01 and DNS-01?
HTTP-01 proves control by serving a token from /.well-known/acme-challenge/ on port 80. DNS-01 proves control by publishing a TXT record. HTTP-01 suits a public web server. DNS-01 suits wildcards, internal hosts, and setups where port 80 isn't reachable.
Which ACME challenge do I need for a wildcard certificate?
DNS-01. Wildcard certificates can only be validated with a DNS TXT record, so HTTP-01 and TLS-ALPN-01 won't work. That's general ACME behavior. Domainee does not support wildcard domains at the moment.
Why did my ACME challenge fail?
Usually one of six causes: the TXT record is missing or on the wrong name, stale or duplicate TXT values, propagation lag, a wrong or delegated zone, a CAA record blocking the CA, or CA rate limits. Run the free TXT lookup and propagation checker first.
How long should I wait for DNS propagation before retrying?
Wait at least the record's TTL plus the zone's negative-caching TTL, then retry once. Use a DNS propagation checker to confirm the value is visible in multiple locations first. Rapid retries don't speed anything up and can trigger CA rate limits.
Can a CAA record cause an ACME challenge to fail?
Yes. If CAA records exist for a domain and none authorizes the CA, issuance is refused even when the challenge itself passes. Check with dig CAA, and add 0 issue "letsencrypt.org" if needed. Domainee's preflight warns on CAA records that block Let's Encrypt.
Can I delegate _acme-challenge with a CNAME?
Yes. Add a permanent CNAME from _acme-challenge.<hostname> to a zone you control. Your ACME client writes the TXT record there, and the CA follows the CNAME to validate. You keep credentials out of the main zone, but you must keep the delegated zone alive.
Do my customers need to add a TXT record for a custom domain on my SaaS?
With Domainee, the customer-facing setup is one CNAME pointing at edge.domainee.dev, or two anycast A records where an apex provider disallows CNAME. Certificates are issued via Let's Encrypt on first request and renewed automatically.
How do I check that the _acme-challenge TXT record is visible?
Run dig TXT _acme-challenge.<hostname> +short, or use the free TXT record lookup. Then run the DNS propagation checker to confirm the value shows up across locations. If the lookup returns nothing, the CA won't see it either.
Next step
Run your hostname through the free TXT record lookup and the DNS propagation checker to see what the CA sees.
If you issue certificates for customer-owned domains, create your first custom domain with one API call on the free tier. That's 20 domains and 100 GB a month, and the card on file is charged $0. Create an account and ship it on a Tuesday afternoon.