cloud-devops

cert-manager Certificate Stuck Pending? Debug & Fix (2026)

July 8, 2026

cert-manager Certificate Stuck Pending? Debug & Fix (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 → Challenge chain with kubectl 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-certificate annotation
  • 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

LimitProduction valueRefill rate
New Orders per Account300 / 3 hours1 every 36 seconds
New Certificates per Registered Domain50 / 7 days (global, all accounts)1 every 202 minutes
New Certificates per Exact Set of Identifiers5 / 7 days1 every 34 hours
Authorization Failures per Identifier/Account5 / hour1 every 12 minutes
Consecutive Authorization Failures per Identifier/Account1,152 total, then paused1 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

  1. cert-manager, "Troubleshooting," https://cert-manager.io/docs/troubleshooting/ — official resource chain diagram, kubectl describe guidance, and exact status/event message formats. Fetched 2026-07-08. 2 3 4 5 6 7 8

  2. 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-certificate and http01-edit-in-place annotations. Fetched 2026-07-08. 2 3 4 5 6 7 8 9 10 11

  3. 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

  4. 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

  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

Frequently Asked Questions

Because a resource in its issuance chain — usually a Challenge — hasn't finished validating domain ownership yet. Run kubectl describe down the Certificate → CertificateRequest → Order → Challenge chain to find the specific blocked resource and its error message. 1