Skip to main content
RunBook Academy

Secrets, PKI & CertificatesX · ACME and Certificate AutomationAutomation

Let's Encrypt in production — staging, profiles, rate limits and ARI

Advanced⏱ ~26 min🧪 Lab requiredcertbotcurl

What you'll learn

  • Exercise a client against the staging environment before it touches production limits
  • Read certificate lifetime from the advertised profile instead of assuming a fixed value
  • Budget issuance against the token-bucket rate limits and their refill rates
  • Schedule renewal from ARI and explain why ARI-coordinated orders escape the limits

Prerequisites

Practice

Verified against OpenSSL 3.5.x teaching target; 3.0+ minimum · OpenSSH 10.x teaching target; 8.2+ minimum for certificate workflows · OpenBao 2.6.x · Smallstep step-ca 0.30.x · Certbot / Pebble Certbot current release; Pebble 2.10.x ACME test server · Kubernetes (cross-course target) 1.36.x · PostgreSQL 17.x · 2026-08-26

Not yet marked complete on this device.

Once a client works, the interesting problems stop being protocol problems and start being capacity and scheduling problems. A public certificate authority is a shared service with published quotas, a published timetable for shortening certificate lifetimes, and no mechanism for lifting a limit you have already exhausted. Everything in this lesson exists because of that last fact.

Staging exists to absorb your mistakes

Let’s Encrypt runs a separate environment with its own accounts, its own quotas and its own untrusted roots. Point a client at it by directory URL:

# Prove the client, the challenge path and the deploy hook here
# before a single production quota is consumed.
STAGING=https://acme-staging-v02.api.letsencrypt.org/directory
certbot certonly --standalone --server "$STAGING" -d api.example.com

Certificates from staging chain to roots with deliberately silly names, currently including Pretend Pear X1 and Bogus Broccoli X2, and no client trusts them. That is the point: an accidental promotion of a staging certificate into production fails loudly at the first handshake instead of quietly serving something nobody validated. Staging quotas are roughly two to three orders of magnitude looser than production, which is what makes it safe to loop a broken client while you fix it.

Staging also ships a deliberate piece of grit. Its directory object contains a junk key whose value is a link to a community post explaining that it is there on purpose, to force clients to tolerate fields they do not recognise. A client that fails on that key would eventually fail in production the first time a new field is added.

The lifetime comes from the profile, and it is changing

There is no single Let’s Encrypt certificate lifetime. There are profiles, advertised under meta.profiles in the directory, and the client selects one by name in the newOrder payload. As of 2026-08-26 three exist:

Propertyclassictlsservershortlived
Validity90 days45 days160 hours
Pending authorization lifetime7 days1 hour1 hour
Authorization reuse period30 days7 hours7 hours
Order lifetime7 days8 hours8 hours
Subject Common Name presentyesnono
Maximum identifiers1002525
Identifier typesDNSDNSDNS and IP

classic is the profile chosen for any order that does not ask for one. tlsserver drops the Common Name as redundant with the subjectAltName and not recommended by the Baseline Requirements, drops keyEncipherment because it is only relevant to cipher suites without forward secrecy, and shortens validity in preparation for the coming 47-day ceiling. shortlived is the same certificate with a lifetime short enough to qualify as a short-lived subscriber certificate, and it is the only profile that can carry IP address identifiers.

Rate limits are token buckets you cannot reset

Limits are evaluated per request using a token bucket, so the usable question is not “what is my weekly allowance” but “how fast does the bucket refill”. The ones that shape real designs:

LimitValueRefill
New certificates per registered domain50 per 7 days, global across all accounts1 per 202 minutes
New certificates per exact set of identifiers5 per 7 days, global1 per 34 hours
New orders per account300 per 3 hours1 per 36 seconds
Authorization failures per identifier per account5 per hour1 per 12 minutes
Consecutive authorization failures per identifier per account1,1521 per day, reset to zero on success

The first row is the one that surprises people, because it is scoped to the registered domain and is global across every account. Splitting your automation into ten ACME accounts does not give you ten times the budget for example.com. Nor does tidying up afterwards help: revoking certificates does not reset rate limits, and there is no mechanism to clear one on request. Overrides can be requested for new orders per account and new certificates per registered domain, and cannot be requested at all for the exact-identifier-set or authorization-failure limits.

The consecutive-failure counter deserves its own alert. It is 1,152 deep and refills at one per day, so a client stuck in a retry loop against a broken challenge burns through it and then pauses the account for that identifier until it is manually unpaused through the self-service portal. A retry loop is not a harmless thing here.

ARI decides when to renew, and buys you an exemption

ACME Renewal Information is RFC 9773, a Proposed Standard published in June 2025, and Let’s Encrypt has served it since March 2023. A server advertises it by publishing renewalInfo in the directory. The request is an unauthenticated GET, with no JWS and no nonce, to a path the client builds from the certificate it already holds:

url = renewalInfo || '/' ||
      base64url(AKI keyIdentifier) || '.' || base64url(Serial)

All trailing equals characters are stripped from both halves, and the serial is the DER-encoded serial number field without its tag and length bytes, which means a leading zero padding byte stays in. The response carries a suggested window, and optionally a URL explaining why:

{
  "suggestedWindow": {
    "start": "2026-09-20T09:00:00Z",
    "end": "2026-09-24T09:00:00Z"
  },
  "explanationURL": "https://acme.example.com/incident/2026-09"
}
flowchart LR
    A["Certificate issued"] --> B["GET renewalInfo\ntwice a day"]
    B --> C{"Window returned"}
    C -- "start in the future" --> D["Pick a uniform random\ntime inside the window"]
    C -- "window already begun" --> E["Renew now"]
    D --> F["Sleep until Retry-After\nthen re-check"]
    F --> B
    E --> G["newOrder with replaces\nexempt from rate limits"]

The recommended client algorithm is deliberate about the random choice: fetch the window, select a uniform random time inside it, renew immediately if that time is already past, otherwise schedule for it, and otherwise sleep until the Retry-After header says and check again. The randomisation is what stops a fleet of thousands of certificates renewing in the same minute, and the server can widen or shift the window to move load or to pull an affected population forward during a compliance incident.

Two details bite implementers. Retry-After here does not mean what it means elsewhere in HTTP: it is the desired amount of time to wait, both minimum and maximum, not a floor. And a renewal order should carry a replaces field naming the certificate it supersedes; reusing the same predecessor twice returns HTTP 409 with the problem type alreadyReplaced.

The operational payoff is large. An order that carries replaces, matches at least one identifier of the certificate being replaced, and names a certificate not already replaced, is exempt from all rate limits. That exemption is what makes a large estate on short lifetimes viable at all. Renewals that do not use ARI are exempt only from the new-orders-per-account and new-certificates-per-registered-domain limits, and remain fully subject to the authorization-failure limits.

Failure has no postman

Let’s Encrypt stopped sending expiry notification emails on 2025-06-04 and deleted the stored addresses. Nothing external will tell you that a renewal stopped. When an order does fail, the reason arrives in the problem document, and policy refusals are explicit:

Order included an identifier for which issuance is forbidden by policy: "blocked-domain.example"

That is a terminal error and retrying will never fix it. By contrast an overall per-IP request limit returns HTTP 503 with a Retry-After header, which a client should honour, and a challenge failure should not be retried aggressively at all, because the server does not implement challenge retries or a Retry-After header on challenge responses.

Production discipline

  1. Make staging the default for anything unproven. A client change, a new challenge type or a new deploy hook goes to staging first, every time, because the production budget is shared with every service in the estate.
  2. Derive every threshold from the certificate in hand. Read notAfter, or read the ARI window; never assume a lifetime.
  3. Alert on authorization failures, not only on expiry. The consecutive-failure counter is the early warning that a retry loop is heading for an account pause.
  4. Send replaces on every renewal. It is the difference between an exempt order and one competing with the rest of your estate for the same bucket.
  5. Count certificates per registered domain, not per host. The limit that will actually stop you is scoped to the registrable name and shared across all your accounts.

Cross-course references

  • Observability for Production Sysadmins - Part XVIII (AlertingRules) covers rule lint and hysteresis, which is what keeps an ARI-driven renewal window from producing a flapping alert on every fetch.
  • Linux for Production Sysadmins - Part XXXVI (Scheduling) covers job logging and failure detection for scheduled work, the layer that decides whether a failed renewal is noticed.
  • Kubernetes for Production Sysadmins - Part LXXVI (Certs) covers a cluster certificate estate whose renewal sequence faces the same batching and margin questions described here.

Quiz

Knowledge check · 4 questions

  1. Q1. A team has exhausted the new-certificates-per-registered-domain limit for example.com. Which action actually restores issuance capacity?

  2. Q2. In an ARI response, the Retry-After header means the minimum time a client must wait before fetching renewal information again, exactly as it does elsewhere in HTTP.

  3. Q3. Explain what an ACME client must include in a renewal order to qualify for the ARI rate-limit exemption, and what the exemption covers.

  4. Q4. Diagnose why issuance has stalled and design the fix.

    An estate holds 300 hostnames under example.com, each with its own certificate on the 45-day tlsserver profile, renewed by a nightly job at 02:00 with no ARI support. On 2026-08-24 a DNS change broke the challenge path for six hours and the night's renewals failed. The catch-up run on 2026-08-25 issued as many as it could and then began returning rate-limit errors, and by 2026-08-26 forty certificates are inside their final week.

Passing score: 75%. Answers are checked in this browser.