| 1 | --- |
| 2 | title: Troubleshooting |
| 3 | description: Diagnose failed DNS challenges, rejected orders, rate limits, and runtime certificate operations. |
| 4 | --- |
| 5 | |
| 6 | Start with the error tag and the affected identifier. Verify your DNS path with |
| 7 | Let's Encrypt staging before repeatedly placing production orders. Preserve |
| 8 | sanitized error metadata, but keep account keys, certificate keys, EAB HMAC |
| 9 | values, and authorization headers out of logs. |
| 10 | |
| 11 | ## Missing DNS solver |
| 12 | |
| 13 | `ACME.DnsSolverNotRegistered` means the descriptor's `type` has no registered |
| 14 | factory in the stack's provider context. For Cloudflare, register both providers: |
| 15 | |
| 16 | ```typescript |
| 17 | const providers = Layer.mergeAll(ACME.providers(), Cloudflare.providers()); |
| 18 | ``` |
| 19 | |
| 20 | Pass this layer as the stack's `providers`. For runtime issuance, use |
| 21 | `acmeDnsSolver(dns)` with a bound DNS client instead of passing the deploy-time |
| 22 | descriptor returned by `AcmeSolver`. |
| 23 | |
| 24 | ## DNS API access fails |
| 25 | |
| 26 | `ACME.DnsSolverError` wraps a failure publishing or cleaning up a challenge. |
| 27 | Check the zone ID, credential scope, and whether the DNS name belongs to that |
| 28 | zone. An API credential valid for one Cloudflare account is not necessarily |
| 29 | valid for another account's zone. |
| 30 | |
| 31 | For runtime `WriteDnsHttp`, the deployment identity must also be allowed to |
| 32 | create the scoped token. A failed cleanup is operationally significant even |
| 33 | if the CA validated the name; inspect the TXT records and retry cleanup safely. |
| 34 | |
| 35 | ## TXT propagation times out |
| 36 | |
| 37 | `ACME.DnsPropagationTimeout` identifies the challenge name that was not observed |
| 38 | within the configured budget. Check authoritative delegation, the zone ID, the |
| 39 | exact TXT value, and whether there is an existing CNAME at that name. Public |
| 40 | recursive DNS may still have a negative or previous-value cache entry. |
| 41 | |
| 42 | The deploy-time Cloudflare timeout defaults to 90 seconds and includes the |
| 43 | 60-second post-visibility delay. A timeout shorter than the delay cannot work. |
| 44 | Lookup attempts are also bounded; increasing `timeout` alone does not remove |
| 45 | that cap. See [DNS validation](/acme/dns-validation#adjust-the-propagation-budget). |
| 46 | |
| 47 | ## The CA rejects a challenge |
| 48 | |
| 49 | `ACME.ChallengeFailed` carries the identifier, CA problem type, and detail. |
| 50 | `ACME.OrderInvalid` carries the order URL and reported problems. A successful |
| 51 | DNS API write does not prove that every CA validator sees the record. |
| 52 | |
| 53 | For an "incorrect TXT record" failure, inspect stale records and cached values |
| 54 | before shortening propagation delays or retrying. Check CAA policy if the CA |
| 55 | reports that it is not authorized to issue. Wildcards require DNS-01 support; |
| 56 | `ACME.ChallengeUnsupported` reports the challenge types the CA offered instead. |
| 57 | |
| 58 | ## An order times out |
| 59 | |
| 60 | `ACME.OrderTimeout` means polling did not reach a terminal state within the |
| 61 | implementation's bounded polling budget. Keep the order URL and status for |
| 62 | investigation. The CA may finish processing later; immediately starting another |
| 63 | order can consume additional issuance allowance without resolving the original |
| 64 | problem. |
| 65 | |
| 66 | ## Rate limits |
| 67 | |
| 68 | The SDK exposes `AcmeRateLimited` and an optional `retryAfter` Effect `Duration`. |
| 69 | Honor that hint when scheduling the next attempt. The [runtime Worker example](/acme/runtime#assemble-the-worker) |
| 70 | returns HTTP 429 and a `Retry-After` header when the hint is available. |
| 71 | |
| 72 | SDK retries apply to individual protocol requests, not a durable end-to-end |
| 73 | certificate job. If you need protocol errors to surface without the default |
| 74 | request retries, add `@distilled.cloud/acme` as a direct dependency and provide |
| 75 | its retry policy around the operation: |
| 76 | |
| 77 | ```typescript |
| 78 | import * as Acme from "@distilled.cloud/acme"; |
| 79 | |
| 80 | const issued = yield* issuer.issue({ |
| 81 | identifiers: ["api.example.com"], |
| 82 | solver, |
| 83 | }).pipe(Acme.Retry.none); |
| 84 | ``` |
| 85 | |
| 86 | Disabling retries does not bypass the CA's limits. Keep renewal jobs serialized, |
| 87 | reuse valid stored certificates, and avoid an unbounded retry around `issue`. |
| 88 | |
| 89 | ## Staging certificates are untrusted |
| 90 | |
| 91 | A Let's Encrypt staging certificate can be correctly issued and still fail |
| 92 | browser trust checks. Use staging to validate your flow, then switch to a |
| 93 | production CA in the intended stage. Uploading a staging chain to a public |
| 94 | consumer does not make it trusted. |
| 95 | |
| 96 | ## ZeroSSL rejects account registration |
| 97 | |
| 98 | Confirm that `eab.keyId` and `eab.hmacKey` are the matching CA-issued pair. The |
| 99 | HMAC value is not your ZeroSSL API key. Keep its base64url encoding intact, load |
| 100 | it as a redacted secret, and verify that the account uses `ACME.ZeroSSL` rather |
| 101 | than another CA's directory. |
| 102 | |
| 103 | ## A CA works locally but fails in a Worker |
| 104 | |
| 105 | Check connectivity from the deployed host, not only from your laptop. Live tests |
| 106 | encountered Let's Encrypt TLS failures from Cloudflare Workers, so the deployed |
| 107 | example uses ZeroSSL. For private CAs, `trustedRoot` currently relies on Bun's |
| 108 | fetch TLS option and is not a portable Worker trust-store override. |
| 109 | |
| 110 | A Cloudflare runtime solver waits 60 seconds by default. Ensure the request and |
| 111 | client remain alive long enough. Use a durable job design for issuance that must |
| 112 | continue independently of a disconnected HTTP client. |
| 113 | |
| 114 | ## A deployed certificate has not renewed |
| 115 | |
| 116 | Check that a deployment actually ran against the correct stack and stage with |
| 117 | access to the same state. Renewal is evaluated on deployment; it has no |
| 118 | background scheduler. Verify that `notAfter` is within `renewBefore`, then check |
| 119 | whether the consumer loaded the updated output. |
| 120 | |
| 121 | The resource's read operation uses persisted certificate attributes rather than |
| 122 | checking revocation status at the CA. If you revoked a certificate out of band, |
| 123 | do not assume the next read will detect it and issue a replacement automatically. |
| 124 | |
| 125 | ## Certificate and private key do not match |
| 126 | |
| 127 | Use the certificate's `chain` and `privateKey` from the same issuance result. |
| 128 | The account private key signs ACME requests and cannot serve as the TLS key. |
| 129 | After renewal, update both values together and reload the TLS consumer. |