Secrets, PKI & CertificatesX · ACME and Certificate AutomationAutomation
Let's Encrypt in production — staging, profiles, rate limits and ARI
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
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:
| Property | classic | tlsserver | shortlived |
|---|---|---|---|
| Validity | 90 days | 45 days | 160 hours |
| Pending authorization lifetime | 7 days | 1 hour | 1 hour |
| Authorization reuse period | 30 days | 7 hours | 7 hours |
| Order lifetime | 7 days | 8 hours | 8 hours |
| Subject Common Name present | yes | no | no |
| Maximum identifiers | 100 | 25 | 25 |
| Identifier types | DNS | DNS | DNS 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:
| Limit | Value | Refill |
|---|---|---|
| New certificates per registered domain | 50 per 7 days, global across all accounts | 1 per 202 minutes |
| New certificates per exact set of identifiers | 5 per 7 days, global | 1 per 34 hours |
| New orders per account | 300 per 3 hours | 1 per 36 seconds |
| Authorization failures per identifier per account | 5 per hour | 1 per 12 minutes |
| Consecutive authorization failures per identifier per account | 1,152 | 1 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
- 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.
- Derive every threshold from the certificate in hand. Read notAfter, or read the ARI window; never assume a lifetime.
- 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.
- Send
replaceson every renewal. It is the difference between an exempt order and one competing with the rest of your estate for the same bucket. - 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
Q1. A team has exhausted the new-certificates-per-registered-domain limit for example.com. Which action actually restores issuance capacity?
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.
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.
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.