cert-manager Certificate Stuck Pending? Debug & Fix (2026)
July 8, 2026
A cert-manager Certificate stays pending because a downstream resource — its CertificateRequest, ACME Order, or a Challenge — hasn't validated yet. Run kubectl describe down that chain to find the exact blocked resource, then fix the HTTP-01/DNS-01 misconfiguration or Let's Encrypt rate limit it names.1
TL;DR
cert-manager issues a certificate through four linked resources: Certificate creates a CertificateRequest, which — for ACME issuers like Let's Encrypt — creates an Order, which creates one Challenge per domain name.1 "Stuck in pending" almost always means one of those Challenge resources hasn't passed its self-check yet, and kubectl describe challenge <name> will tell you exactly why: a firewall blocking the HTTP-01 solver, a DNS-01 record that hasn't propagated, or an ACME account/rate-limit error surfaced further up the chain. This post walks the full debug chain with the real cert-manager 1.20 output at each step, covers the cert-manager.io/issue-temporary-certificate workaround for tls: handshake failure errors, and explains when a Let's Encrypt rate limit — not a config bug — is the real cause.23
What you'll learn
- How to walk the
Certificate → CertificateRequest → Order → Challengechain withkubectl describe - What the exact "pending" and self-check error messages mean at each step
- The difference between an HTTP-01 and a DNS-01 challenge failure, and how to debug each
- When to use the
cert-manager.io/issue-temporary-certificateannotation - Why cert-manager only processes 60 challenges at once, and what that means for bulk cert rollouts
- Whether a Let's Encrypt rate limit — not your YAML — is actually blocking issuance
- Why you should point cert-manager at the Let's Encrypt staging environment before debugging in production
Why is my cert-manager certificate stuck in pending?
Your Certificate is stuck because the resource it depends on hasn't reached a terminal state. cert-manager's own docs put it plainly: "when troubleshooting cert-manager your best friend is kubectl describe... it is not advised to use the logs as these are quite verbose."1 Start at the top:
$ kubectl get certificate
NAME READY AGE
example-com-tls False 1h
READY=False just means issuance isn't finished — it's not itself an error. Describe it to see the current status and event trail:
$ kubectl describe certificate example-com-tls
Status:
Conditions:
Last Transition Time: 2026-07-08T09:12:03Z
Message: Issuing certificate as Secret does not exist
Reason: DoesNotExist
Status: False
Type: Ready
Next Private Key Secret Name: example-com-tls-wtlww
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Issuing 105s cert-manager Issuing certificate as Secret does not exist
Normal Generated 105s cert-manager Stored new private key in temporary Secret resource "example-com-tls-wtlww"
Normal Requested 104s cert-manager Created new CertificateRequest resource "example-com-tls-bw5t9"
The last event names the CertificateRequest cert-manager created. That's your next stop.
How do I debug the CertificateRequest, Order, and Challenge chain?
Follow the chain one resource at a time — each one's describe output names the next resource down.1 The full chain, straight from cert-manager's own architecture diagram, is:
Ingress (optional) --> Certificate --> CertificateRequest --> Order --> Challenge
(ACME issuers only)
Order and Challenge only exist for ACME issuers (Let's Encrypt, ZeroSSL, and similar); internal CA or Vault issuers stop at CertificateRequest.1 Describe the CertificateRequest:
$ kubectl describe certificaterequest example-com-tls-bw5t9
Spec:
Issuer Ref:
Group: cert-manager.io
Kind: ClusterIssuer
Name: letsencrypt-production
Status:
Conditions:
Message: Waiting on certificate issuance from order example-com-tls-fqtfg-1: "pending"
Reason: Pending
Status: False
Type: Ready
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal OrderCreated 8m20s cert-manager Created Order resource example-com-tls-fqtfg-1
That Waiting on certificate issuance from order ...: "pending" message is cert-manager's exact, literal wording — it's a pointer, not the root cause.1 Describe the named Order next:
$ kubectl describe order example-com-tls-fqtfg-1
State: pending
URL: https://acme-v02.api.letsencrypt.org/acme/order/41123272/265506123
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Created 1m cert-manager Created Challenge resource "example-com-tls-fqtfg-1-0" for domain "example.com"
Finally, kubectl get challenges shows the actual reason issuance is blocked:
$ kubectl get challenges
NAME STATE DOMAIN REASON AGE
example-com-tls-fqtfg-1-0 pending example.com Waiting for dns-01 challenge propagation 22s
That REASON column is where the real debugging starts — and it splits into two very different failure modes depending on whether you're using HTTP-01 or DNS-01.2
What does "Waiting on certificate issuance from order ...: pending" actually mean?
It means the CertificateRequest is waiting on its child Order, which is itself waiting on one or more Challenge resources to finish validating domain ownership — it is not an error on its own.1 Both HTTP01 and DNS01 challenges go through a two-stage process: cert-manager first performs an internal "self check" before ever telling the ACME server the challenge is ready, specifically so it doesn't burn ACME rate limits on challenges that would obviously fail due to DNS or load-balancer propagation delay.2 Only after the self-check passes does cert-manager tell Let's Encrypt to validate, at which point the Challenge status updates to valid and the chain unwinds back up to Certificate.
HTTP-01 vs DNS-01: which challenge type is failing, and how do I debug each?
Look at the Challenge's domain and reason string — kubectl describe challenge shows which solver mechanism is in use and exactly what's failing.2 The two solvers fail for almost entirely different reasons:
HTTP-01 proves ownership by serving a token at http://<domain>/.well-known/acme-challenge/<token>. If the self-check fails, you'll see a Reason like:
Waiting for http-01 challenge propagation: failed to perform self check GET request
'http://example.com/.well-known/acme-challenge/_fgdLz0i3TFiZW4LBjuhjgd5nTOkaMBhxYmTY':
Get "http://example.com/...": remote error: tls: handshake failure
Work through this checklist in order: confirm the challenge URL is reachable from the public internet, then confirm it's reachable from inside a pod in your cluster (not just from your laptop) — cert-manager's docs specifically call out testing from inside a Pod, since cluster-internal network paths can differ from the public internet.2 A 404 on that URL means the ACME solver pod isn't running or the Ingress the solver created isn't wired up — check with kubectl describe ingress on the HTTP-01 solver Ingress.2 A tls: handshake failure means your ingress controller has nothing to serve on port 443 yet for that host — see the temporary-certificate fix below.
DNS-01 proves ownership by publishing a _acme-challenge.<domain> TXT record. If that record exists (verify with dig or your DNS provider's console) but cert-manager still reports the challenge as pending, the most common cause is that cert-manager's in-cluster DNS resolver can't see the record yet — even though the public internet can.2 A second, sneakier DNS-01 failure mode: cert-manager uses SOA (Start of Authority) records to figure out which DNS zone to write the TXT record into, and some resolvers (notably dnsmasq with the --filterwin2k flag, which OpenWRT/LuCI expose as a "Filter useless" option) strip SOA records entirely, causing cert-manager to misidentify the zone.2 The fix in both cases is to point cert-manager's DNS-01 self-check at a different set of nameservers rather than the cluster's default resolver.
What's the cert-manager.io/issue-temporary-certificate annotation, and when do I need it?
Use it when your HTTP-01 self-check fails with tls: handshake failure — it tells cert-manager to issue a temporary, self-signed certificate immediately so your ingress controller has something valid to serve on port 443 while the real ACME certificate is still being issued.2 Set it directly on the Ingress or Certificate resource:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-com
annotations:
cert-manager.io/issue-temporary-certificate: "true"
cert-manager.io/cluster-issuer: letsencrypt-production
spec:
# ... rules, tls, etc.
If you're behind Google Cloud Load Balancer (GKE), cert-manager's own docs recommend pairing it with a second annotation, acme.cert-manager.io/http01-edit-in-place: "true", which stops GCLB from allocating a second IP address for the HTTP-01 solver endpoint:2
cert-manager.io/issue-temporary-certificate: "true"
acme.cert-manager.io/http01-edit-in-place: "true"
Why does cert-manager only process 60 challenges at a time, and does that matter for me?
Is a Let's Encrypt rate limit blocking my certificate, not a config bug?
Sometimes — and it's worth ruling out before you spend an hour debugging DNS. Let's Encrypt enforces several separate production limits, and hitting any one of them will leave your Order or Challenge stuck without ever going invalid:3
| Limit | Production value | Refill rate |
|---|---|---|
| New Orders per Account | 300 / 3 hours | 1 every 36 seconds |
| New Certificates per Registered Domain | 50 / 7 days (global, all accounts) | 1 every 202 minutes |
| New Certificates per Exact Set of Identifiers | 5 / 7 days | 1 every 34 hours |
| Authorization Failures per Identifier/Account | 5 / hour | 1 every 12 minutes |
| Consecutive Authorization Failures per Identifier/Account | 1,152 total, then paused | 1 refill/day |
A registered domain here means the part you bought from your registrar — example.com, not app.example.com.3 Two behaviors catch people out repeatedly: revoking a certificate does not reset any rate limit, because the resources spent issuing it are already consumed, and reinstalling your ACME client or wiping its local state between attempts is one of the most common ways to accidentally burn through the 5-per-week "exact set of identifiers" limit, since each fresh attempt looks like a brand-new request rather than a renewal.3 If your client supports ACME Renewal Info (ARI), renewals that explicitly reference the certificate they're replacing are exempt from every rate limit; older non-ARI clients only get a partial exemption (from the per-account and per-domain limits, but not the per-identifier-set or failure limits) as long as the requested domain set exactly matches the certificate being renewed, ignoring capitalization and the order of the names.3
Should I test against the Let's Encrypt staging environment first?
Yes, for any new issuer configuration, DNS-01 webhook, or bulk rollout — staging uses the same ACME protocol but with dramatically higher limits so you can iterate without burning production capacity.4 Point your ClusterIssuer at the staging ACME directory:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: you@example.com
privateKeySecretRef:
name: letsencrypt-staging-key
solvers:
- http01:
ingress:
ingressClassName: nginx
Staging's New-Certificates-per-Registered-Domain limit is 30,000 per second (versus 50 per week in production), and Authorization-Failures-per-Identifier-per-Account is 200/hour (versus 5/hour).4 The one catch: staging certificates chain up to roots explicitly named things like "(STAGING) Pretend Pear X1" and are not trusted by any real browser or client — don't add the staging root to any trust store you use for anything besides testing.4 If your workflow is CI rather than manual iteration, Let's Encrypt explicitly recommends Pebble — its purpose-built local ACME test server — over staging, since staging still makes real network calls and offers no way to fake DNS or challenge success for reproducible test runs.4
Bottom line
A stuck cert-manager Certificate is a chain-of-custody problem, not a mystery: kubectl describe down through CertificateRequest, Order, and Challenge will name the exact blocked resource and its error nearly every time, and that error almost always falls into one of three buckets — an HTTP-01 network/ingress misconfiguration, a DNS-01 propagation or zone-detection issue, or a Let's Encrypt rate limit you didn't know you were near. Debug new issuer configs against the Let's Encrypt staging environment first, and reach for the issue-temporary-certificate annotation only as a stopgap for the specific tls: handshake failure case, not as a general fix. For related cluster-debugging workflows, see our guides on debugging pods with ephemeral containers when kubectl exec has no shell, safe idempotent Helm upgrades, and automating HTTPS certificates outside Kubernetes with Caddy.
Footnotes
-
cert-manager, "Troubleshooting," https://cert-manager.io/docs/troubleshooting/ — official resource chain diagram,
kubectl describeguidance, and exact status/event message formats. Fetched 2026-07-08. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
cert-manager, "Troubleshooting Problems with ACME / Let's Encrypt Certificates," https://cert-manager.io/docs/troubleshooting/acme/ — Order/Challenge debugging steps, HTTP-01/DNS-01 failure modes,
issue-temporary-certificateandhttp01-edit-in-placeannotations. Fetched 2026-07-08. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 -
Let's Encrypt, "Rate Limits," https://letsencrypt.org/docs/rate-limits/ (last updated June 12, 2025) — production rate limit values, ARI renewal exemptions, revocation-does-not-reset-limits policy. Fetched 2026-07-08. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Let's Encrypt, "Staging Environment," https://letsencrypt.org/docs/staging-environment/ (last updated April 10, 2026) — staging ACME URL, staging rate limit values, untrusted staging root names, Pebble recommendation for CI. Fetched 2026-07-08. ↩ ↩2 ↩3 ↩4 ↩5
-
cert-manager, "ACME Orders and Challenges," https://cert-manager.io/docs/concepts/acme-orders-challenges/ — challenge scheduling, 60-challenge default concurrency, 10-second self-check retry interval, conflict detection rules. Fetched 2026-07-08. ↩ ↩2