# Heroku ACM: automated certificates for custom domains

> Set up Heroku ACM for custom domains: herokudns.com DNS targets, CAA for Let's Encrypt, ACM statuses, failure reasons and fixes.

Source: https://ssl.sh/guides/heroku-automated-certificate-management/
Last updated: 2026-11-10

Heroku Automated Certificate Management (ACM) gets and renews Let's Encrypt certificates for your app's custom domains. Add each domain, point DNS at the `herokudns.com` target Heroku gives you, and ACM issues the certificate, usually within 45 to 60 minutes.

ACM is free for all apps, and certificates renew one month before they expire. Uploading your own certificate switches ACM off for that app. Steps follow the Heroku Dev Center as of November 2026, with `example-app` and `example.com` as examples.

## At a glance

| Question | Heroku |
|---|---|
| CA for automatic certificates | Let's Encrypt (via ACM) |
| Price | Included for all applications |
| Subdomain (`www.example.com`) | `CNAME` to its `<haiku>.herokudns.com` DNS target |
| Root (`example.com`) | `ALIAS`/`ANAME`/flattened `CNAME` to its DNS target; A records are not supported |
| CAA must allow | `letsencrypt.org` |
| Wildcard via ACM | Not in the Common Runtime docs; Private Space rules below |
| Upload your own certificate | Yes, `heroku certs:add` (disables ACM) |
| Renewal | Automatic, one month before expiry |
| TLS versions | TLS 1.2 and TLS 1.3 only |

## Step 1: Add the domains

Add the root and `www` names separately; Heroku requires root domains to be added in addition to any subdomains:

```bash
heroku domains:add www.example.com -a example-app
heroku domains:add example.com -a example-app
heroku domains -a example-app
```

Each domain gets its own DNS target, for example `whispering-willow-5678.herokudns.com`. Your Heroku account must be verified to add domains, and adding domains costs nothing.

## Step 2: Point DNS at the targets

Point each name at its own `herokudns.com` target: a CNAME for `www`, and an ALIAS, ANAME or flattened CNAME for the root.

| Name | Record | Value |
|---|---|---|
| `www.example.com` | `CNAME` | `whispering-willow-5678.herokudns.com` |
| `example.com` | `ALIAS`, `ANAME` or flattened `CNAME` | `hidden-sierra-7936.herokudns.com` |

Heroku is explicit that A records are not enough for root domains, because Heroku's IP addresses are dynamic. Providers with ALIAS or ANAME include DNSimple, DNS Made Easy, easyDNS, Namecheap and DreamHost, and Cloudflare flattens a CNAME at the root. If your provider only offers A records at the root, move DNS to one that supports these record types.

Older apps sometimes point at `example-app.herokuapp.com` or `*.herokussl.com`. ACM cannot verify those; switch to the `herokudns.com` target. Also leave out `AAAA` records, which ACM does not support.

## Step 3: Check CAA records

If the domain publishes CAA records, add one that allows Let's Encrypt:

```dns
example.com.  3600  IN  CAA  0 issue "letsencrypt.org"
```

Otherwise ACM fails with **CAA record prevents issuance, CAA record does not match issuer**. If your DNS provider does not support CAA at all, ACM can fail with **DNS provider doesn't support CAA**. Test with the [CAA checker](https://ssl.sh/tools/caa-checker/).

## Step 4: Enable ACM and watch the status

Enable ACM once DNS points at Heroku, then check the status:

```bash
heroku certs:auto:enable -a example-app
heroku certs:auto -a example-app
```

Generating a certificate usually takes 45 to 60 minutes. The statuses are:

| Status | Meaning |
|---|---|
| **Cert Issued** | A certificate has been issued for the domain. |
| **In Progress** | Heroku is verifying the domain's DNS. |
| **DNS Verified** | DNS is verified; the certificate is being generated. Not finished yet. |
| **Waiting** | Heroku is waiting to process the certificate. |
| **Failing** | Heroku cannot verify DNS and will keep trying for up to an hour. |
| **Failed** | Heroku cannot verify DNS and has stopped trying. |

## Wildcards and your own certificate

Wildcard domains can be added to an app with a CNAME from `*` to the wildcard's DNS target. Heroku's ACM article does not describe ACM issuing wildcard certificates in the Common Runtime. For Private Spaces it says you must upload your own wildcard certificate, that Cedar-generation Private Space apps using ACM cannot have wildcard domains, and that a newer generation of Private Space apps can use ACM with wildcard domains.

To use an EV certificate, another CA or a wildcard certificate, upload it:

```bash
heroku certs:add server.crt server.key -a example-app
heroku certs -a example-app
```

Include the intermediate certificates in `server.crt`. Uploading a certificate disables ACM; run `heroku certs:auto:enable` to return to ACM later. Heroku does not let you extract private keys, so keep your own copy. For automatic renewal of a wildcard on a Private Space app, Heroku points to add-ons such as SSL FastTrack or ExpeditedSSL.

## Switching from an uploaded certificate to ACM

ACM uses the same DNS configuration as Heroku SSL (SNI), and enabling it on a Private Space app needs no DNS change. While ACM verifies DNS, the app keeps serving the existing certificate. For Private Space apps Heroku documents this order:

1. Find the uploaded certificate's name with `heroku certs`.
2. Run `heroku certs:auto:enable`.
3. Wait with `watch heroku certs:auto` until the new certificate is issued. Skipping the wait can cause downtime.
4. Remove the old certificate with `heroku certs:remove --name <name>`.

## Verify

Once ACM shows the certificate as issued, each domain presents a Let's Encrypt certificate. Check it with the [SSL checker](https://ssl.sh/tools/ssl-checker/), or:

```bash
curl ssl.sh/example.com
```

Then inspect the certificate:

```bash
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -issuer -subject -ext subjectAltName -dates
```

On macOS, avoid `curl -k` for this check: Heroku notes that on many macOS versions curl with `-k` does not send SNI.

## Renewal

ACM renews certificates one month before they expire and creates a new certificate whenever you add or remove a custom domain. If ACM cannot create a certificate, Heroku sends a notification after three days of attempts. Uploaded certificates are never renewed by Heroku; replace them with `heroku certs:update`. Let's Encrypt itself stopped sending expiry emails on June 4, 2025, and its default lifetime becomes 64 days on February 10, 2027, so add critical domains to [ssl.sh monitoring](https://ssl.sh/monitor/) (in development).

## Troubleshooting

Do not turn ACM off and on to speed things up; that can trigger a Let's Encrypt rate limit. Fix the cause, then run:

```bash
heroku certs:auto:refresh -a example-app
```

Heroku documents these failure reasons:

- **CN exceeding 64 character limit**: the domain name is longer than 64 characters.
- **Incorrect DNS settings**: the record is wrong, the domain fails DNSSEC validation, or a Private Space app has no running web dyno.
- **Unable to resolve DNS for domain**: the name has not propagated; ACM checks Google Public DNS (8.8.8.8).
- **CDN not returning HTTP challenge**: the domain points at a CDN such as CloudFront that returns 404 for the challenge file.
- **DNS redirect not forwarding path**: the DNS provider's forwarding drops the challenge path.
- **Incorrect CNAME target**: the CNAME points at `<app-name>-<random-identifier>.herokuapp.com` instead of the DNS target.
- **Rate limited**: too many certificate requests for the domains in 7 days; wait, then refresh. See [Let's Encrypt rate limits](https://ssl.sh/errors/lets-encrypt-rate-limit/).
- **DNS provider timed out**: refresh to retry.
- **Strict TLS in CDN not supported**: Cloudflare is in front but misconfigured; Heroku recommends not combining ACM with Cloudflare and disabling ACM there.
- **DNS provider doesn't support CAA** and **CAA record prevents issuance, CAA record does not match issuer**: see Step 3 and [CAA record prevents issuance](https://ssl.sh/errors/caa-record-prevents-issuance/).
- **Domain record does not exist**: Let's Encrypt got NXDOMAIN; check the spelling and the CNAME.
- **Domain considered unsafe**: Google Safe Browsing flags the domain.
- **Let's Encrypt domain blocklist**: ask on the Let's Encrypt community forum.
