Contents

Let's Encrypt Rate Limits: What to Do When You Hit the Wall

Why your certificate issuance suddenly fails, and how to dig out

Contents

Let’s Encrypt is one of the genuinely great things to happen to the open web. Free, automated, universally trusted TLS certificates — the whole reason your homelab services have padlocks now instead of a wall of browser warnings. It is also a free service handling staggering volume, and it protects itself with rate limits. The trouble is you only ever learn those limits exist at the precise moment you slam into one, usually at 2am, usually while debugging something else, usually with a renewal cron job hammering away making everything worse.

Let me walk through what the walls actually are, how to tell which one you’ve hit, and how to get yourself back to issuing certificates — and, more usefully, the handful of habits that mean you never see one of these errors in normal operation.

The limit you’ll actually hit

Advertisement

There are several rate limits, but for self-hosters one dominates: Certificates per Registered Domain, currently 50 per week, counted against the registered domain (the example.com part, not each subdomain). Every distinct certificate for *.example.com counts. Spin up Traefik with a typo in your config and let it retry, or run a half-dozen services each requesting their own cert in a loop, and you can burn through fifty in an afternoon.

The error, when it comes, is unmistakable:

1
2
3
4
acme: error: 429 :: POST :: https://acme-v02.api.letsencrypt.org/acme/new-order
:: urn:ietf:params:acme:error:rateLimited ::
too many certificates (50) already issued for "example.com" in the last 168 hours,
retry after 2024-10-12T08:14:22Z

That retry after timestamp is not advisory. Nothing you do — restarting the proxy, deleting cert files, recreating the account — will move it. The window is a rolling 168 hours, so the wall starts dissolving exactly one week after each issuance. There is no support queue, no “please reset my limit” button. You wait.

The second limit worth knowing: Duplicate Certificate, 5 per week for the exact same set of domain names. This one is sneakier because it bites people whose containers don’t persist their certificates. Every redeploy fetches a fresh cert for the identical hostname, and five redeploys later you’re locked out of that specific cert — even though you’re nowhere near the 50-per-domain ceiling.

One nuance that trips people up: these limits aren’t reset in a single lump at the seven-day mark. Since Let’s Encrypt moved to a token-bucket model, the allowance refills gradually. The per-registered-domain bucket tops back up at roughly one certificate every 202 minutes, and the duplicate bucket at about one every 34 hours. In practice that means you don’t have to wait the full week for total relief — if you tripped the wall by three certs, you’ll get one back in a few hours, not seven days. The retry after timestamp in the error is the moment the next token becomes available for your specific request, not the moment your whole allowance resets.

The single most important 2025 development, though, is renewal exemption. If your ACME client speaks ARI (ACME Renewal Information) and the order is recognised as a genuine renewal of an existing certificate, it is exempt from the rate limits entirely. Modern certbot, acme.sh, and the Go lego library that Traefik and Caddy build on all support ARI now. This quietly defuses the most common self-hoster disaster — a renewal storm — provided your client is recent and your certs persist so the renewal is recognisable as one. If you last touched your ACME tooling in 2022, upgrading it is the single highest-leverage thing you can do here.

How to tell which wall you hit

Read the 429 body. “too many certificates (50) … for example.com” is the per-domain limit — you’ve issued too many different certs. “too many certificates already issued for exact set of domains” is the duplicate limit — you’ve issued the same cert too many times. The two have completely different fixes, and confusing them wastes a week.

To check your standing before you get burned, query crt.sh, the public Certificate Transparency log search. Every cert Let’s Encrypt issues is logged there within minutes:

1
2
3
4
5
6
$ curl -s 'https://crt.sh/?q=example.com&output=json' \
    | jq -r '.[] | "\(.entry_timestamp)  \(.name_value)"' \
    | sort -r | head
2024-10-09T07:58:01  service.example.com
2024-10-09T07:52:14  service.example.com
2024-10-09T07:41:09  service.example.com

Count entries inside the last seven days and you know exactly how close to the wall you are. Three identical lines minutes apart is the smoking gun of a redeploy loop chewing through the duplicate limit.

The single most important fix: the staging environment

Advertisement

Almost every rate-limit disaster traces back to testing against production. Let’s Encrypt runs a staging endpoint with limits roughly 10x higher and certificates signed by an untrusted root. You point your client at it while you debug, accept that browsers will complain, and switch to production only once issuance demonstrably works.

With certbot:

1
$ certbot certonly --staging -d service.example.com

With Traefik, it’s one line in your static config:

1
2
3
4
5
6
7
certificatesResolvers:
  letsencrypt:
    acme:
      email: [email protected]
      storage: /etc/traefik/acme.json
      caServer: https://acme-staging-v02.api.letsencrypt.org/directory
      tlsChallenge: {}

Get a green issuance in staging, then comment out caServer (production is the default) and delete the now-untrusted acme.json so it re-issues real certs once. Burn your fifty in staging, not production.

There’s a subtlety here worth calling out because it catches people the second time round: staging and production maintain separate account keys and separate stored certificates. If your client stores staging certs in the same file it’ll use for production, switching the caServer isn’t enough — the client sees an existing (staging) cert, decides it doesn’t need to renew, and you’re left serving an untrusted certificate wondering why the padlock is still angry. That’s why the “delete acme.json” step matters. For certbot the equivalent is that --staging certificates land under a different lineage; use certbot certificates to list what you actually have and certbot delete --cert-name service.example.com to clear a staging leftover before you re-issue for real.

A word on which challenge you’re using

Rate limits and challenge type interact in a way that bites specifically when you’re iterating. If you’re using the HTTP-01 challenge, every failed attempt still counts against the failed validation limit (5 per hostname per hour), and a misconfigured reverse proxy that can’t serve the /.well-known/acme-challenge/ path will chew through that fast — long before you touch the 50-per-week ceiling. If you’re using DNS-01 (needed for wildcards), the failure mode shifts to slow DNS propagation causing timeouts, which again burn validation attempts. The staging environment is where you sort all of this out. If you’re wondering whether to expose these services publicly at all versus keeping issuance internal, I’ve weighed that trade-off in the piece on Cloudflare Tunnels and the ports you don’t open — a tunnel changes which challenge type is even reachable.

Persist, consolidate, back off

Three habits keep you off the wall for good. Persist your certs — mount acme.json, /etc/letsencrypt, or whatever your client stores on a real volume so a redeploy reuses the existing certificate instead of fetching another. This alone kills the duplicate-cert problem entirely.

Consolidate with SANs. One certificate can carry up to 100 names. Instead of ten certs for ten subdomains, request one cert with ten Subject Alternative Names. That’s one issuance against your weekly count, not ten:

1
$ certbot certonly -d a.example.com -d b.example.com -d c.example.com

Back off on failure. A renewal cron that retries every minute after a transient failure is how a small problem becomes a 429. Certbot already renews sane (twice daily, only acting on certs within 30 days of expiry); don’t replace it with something more eager. If you’re hand-rolling, exponential backoff is not optional.

It’s worth stepping back and asking whether Let’s Encrypt is even the right CA for every certificate you issue. For anything that never faces a browser — service-to-service traffic inside your own network — a publicly trusted cert is overkill, and every internal cert you pull from Let’s Encrypt is a cert eating your weekly budget for no benefit. Those connections are better served by your own private CA, which has no rate limits at all because you run it. I go into that pattern properly in mutual TLS between services without a service mesh; the short version is that moving internal traffic to a private CA both hardens it and takes pressure off your public issuance count. Reserve Let’s Encrypt for the certs that genuinely need public trust: the endpoints a real browser will visit.

When you’re already locked out: the triage

Say the worst has happened and you’re staring at a 429 on production right now. Here’s the order I work through it, because panic makes people do destructive things — deleting accounts, wiping cert stores, switching CAs mid-incident — that turn a one-week problem into a two-week one.

First, stop the bleeding. Whatever is requesting certs — Traefik, certbot, a Kubernetes cert-manager, a rogue redeploy loop — pause it. A docker compose stop traefik or scaling a deployment to zero replicas buys you room to think without the retry loop making things worse. Every attempt while you’re locked out is noise in the crt.sh log you’re about to read, and if it’s the failed-validation limit you tripped, retries actively dig the hole deeper.

Second, read the error carefully and check crt.sh, as above, to confirm which wall you hit. Don’t guess. The per-domain and duplicate limits have opposite fixes.

Third, serve something valid in the meantime. Your locked-out services don’t have to sit behind browser warnings for a week. If you have any still-valid certificate — an old one on disk, a wildcard, a cert from a different CA — point the proxy at it temporarily. A cert that’s valid but for a slightly wrong name is often less alarming to users than a hard ACME failure page, and a self-signed cert with a clear internal note beats an outage for a homelab dashboard only you use.

Fourth, fix the root cause in staging while the production window heals. Persist the volume, consolidate to SANs, upgrade your client so ARI kicks in. By the time the token bucket refills, you want the underlying bug gone so you don’t immediately trip the wall again — the classic second-day mistake.

Verdict

Let’s Encrypt’s rate limits are the price of a free, abuse-resistant CA, and they’re entirely livable once you respect them. The whole game is: test in staging, persist your certificates, and consolidate subdomains into SAN certs. Do those three things and you will likely never see a 429 in normal operation. If you’ve already hit the wall, there’s no shortcut — read the error to learn which wall, check crt.sh to confirm, and use the staging endpoint to fix your config while the production window heals itself. Then go to bed; the wall comes down in a week whether you watch it or not.

Advertisement
Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.