---
title: cert-manager Certificate Stuck Pending? Debug & Fix (2026)
description: cert-manager Certificate stuck in pending? Debug the Certificate to Challenge resource chain and fix the HTTP-01, DNS-01, and Let's Encrypt rate-limit causes.
url: https://nerdleveltech.com/cert-manager-certificate-stuck-pending
language: en
date: '2026-07-08'
category: cloud-devops
tags:
  - cert-manager
  - kubernetes
  - lets-encrypt
  - tls
  - acme
  - cloud-devops
  - kubectl
markdown_url: https://nerdleveltech.com/cert-manager-certificate-stuck-pending.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/cert-manager-certificate-stuck-pending'
terms: https://nerdleveltech.com/llms.txt
---

# 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.[^troubleshooting]

## 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.[^troubleshooting] "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.[^acme-troubleshooting][^ratelimits]

## 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."[^troubleshooting] Start at the top:

```console
$ 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:

```console
$ 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.[^troubleshooting] 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`.[^troubleshooting] Describe the `CertificateRequest`:

```console
$ 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.[^troubleshooting] Describe the named `Order` next:

```console
$ 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:

```console
$ 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.[^acme-troubleshooting]

## 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.[^troubleshooting]

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.[^acme-troubleshooting] 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.[^acme-troubleshooting] 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.[^acme-troubleshooting] 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.[^acme-troubleshooting] 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.[^acme-troubleshooting]

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.[^acme-troubleshooting] 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.[^acme-troubleshooting] Set it directly on the Ingress or Certificate resource:

```yaml
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:[^acme-troubleshooting]

```yaml
    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?

By default cert-manager schedules at most **60 challenges concurrently**, cluster-wide — configurable via the controller's `--max-concurrent-challenges` flag, or `maxConcurrentChallenges` in the Helm chart values or a `ControllerConfiguration` file.[^concepts] This matters if you're rolling out TLS for a large batch of Ingresses or domains at once: challenges past the concurrency limit simply queue rather than fail, so a certificate that looks "stuck" might just be waiting its turn.

cert-manager's scheduler also refuses to run two challenges that would validate the *same* target at once — HTTP-01 challenges conflict if they resolve to the same hostname, and DNS-01 challenges conflict if they'd write to the same `_acme-challenge` DNS record — even if they're configured through different ingress classes or DNS provider backends under the hood.[^concepts]

If your self-check keeps failing and retrying, know that cert-manager retries on a fixed **10-second interval** indefinitely, until you either fix the underlying config or delete the `Order` to force a fresh attempt.[^concepts]

## 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`:[^ratelimits]

| 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`.[^ratelimits] 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.[^ratelimits]

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.[^ratelimits]

## 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.[^staging] Point your `ClusterIssuer` at the staging ACME directory:

```yaml
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).[^staging] 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.[^staging] 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.[^staging]

## FAQ

**Why is my cert-manager Certificate stuck in pending?**
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.[^troubleshooting]

**What's the difference between an Order and a Challenge in cert-manager?**
An `Order` represents one ACME certificate request as a whole; it creates and tracks one `Challenge` resource per DNS name being validated inside that order.[^concepts] You never create either resource by hand — cert-manager manages both automatically.

**How many challenges does cert-manager run at once?**
Up to 60 by default, cluster-wide, controlled by `--max-concurrent-challenges` (or Helm's `maxConcurrentChallenges`); challenges beyond that limit queue rather than fail.[^concepts]

**Does revoking a Let's Encrypt certificate reset my rate limit?**
No. Let's Encrypt is explicit that revoking a certificate does not reset any rate limit, since the resources used to issue it were already consumed.[^ratelimits]

**What annotation fixes a `tls: handshake failure` during HTTP-01 validation?**
`cert-manager.io/issue-temporary-certificate: "true"` on the Ingress or Certificate — it issues a temporary self-signed cert so the ingress controller can serve *something* over TLS while the real one is pending; on GKE/GCLB, pair it with `acme.cert-manager.io/http01-edit-in-place: "true"`.[^acme-troubleshooting]

**Should I debug against Let's Encrypt production or staging?**
Staging, for anything experimental — its rate limits are thousands of times higher (30,000/second vs. 50/week for the per-domain limit), but its certificates are not trusted by browsers and must never be used for real traffic.[^staging]

## 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](https://nerdleveltech.com/kubectl-debug-ephemeral-container-no-shell), [safe idempotent Helm upgrades](https://nerdleveltech.com/helm-upgrade-install-idempotent-deploy), and [automating HTTPS certificates outside Kubernetes with Caddy](https://nerdleveltech.com/caddy-reverse-proxy-docker-compose-production-https-tutorial).

[^troubleshooting]: 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.
[^acme-troubleshooting]: 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.
[^concepts]: 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.
[^ratelimits]: 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.
[^staging]: 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.
