- Book: Ship It — the pragmatic startup tech stack
- Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go
- My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools
- Me: xgabriel.com | GitHub
Somewhere on a VPS you set up years ago there is a line like this:
0 3 1 */2 * /opt/scripts/renew-certs.sh
"Every two months, renew the certificate." It has worked since the day you wrote it, because a Let's Encrypt certificate lived 90 days and two months is about 60. That margin is about to shrink to almost nothing. Let's Encrypt announced on October 7, 2026 that from February 10, 2027 every subscriber gets 64-day certificates by default. In 2028 the default drops again, to 45 days. A cron that renews on day 60 of a 64-day certificate has four days of slack. On a 45-day certificate it has none: the certificate is already dead for 15 days when the job wakes up.
What Let's Encrypt actually announced
The schedule comes from two posts on the Let's Encrypt blog: the original 90-to-45 plan from December 2025 and the October 2026 update that confirmed the first step.
| Date | Profile | Lifetime | Authorization reuse |
|---|---|---|---|
| Now | classic (default) | 90 days | 30 days |
| Opt-in since May 13, 2026 | tlsserver | 45 days | — |
| Feb 10, 2027 | classic (default) | 64 days | 10 days |
| Feb 16, 2028 | classic (default) | 45 days | 7 hours |
The change applies to certificates issued or renewed on or after February 10, so you meet it at your first renewal after that date. Let's Encrypt says it expects the last 90-day certificate to expire on May 11, 2027, which means a broken cron can look healthy until spring. The same post schedules the staging environment's switch to 64-day certificates for October 14, 2026, so you can test before production changes. The December post is blunt: "renewing at a hardcoded interval of 60 days will no longer be sufficient." The October post tells you to grep "for common hardcoded numbers like 83, 80 or 60 in cron jobs, wrapper scripts and runbooks."
The shorter authorization reuse matters less than it sounds. The October post says you need no changes unless your ACME client relies on reusing a recent validation, for example a manual DNS-01 challenge you complete once and then reissue against for weeks.
The rest of the industry is heading the same way. The CA/Browser Forum's ballot SC-081v3 cuts the maximum lifetime of every publicly trusted TLS certificate from 398 days to 47 days in steps between March 2026 and March 2029.

The certificate keeps getting shorter. The cron schedule stays where you left it.
The four-day arithmetic
Run the numbers for the cron at the top of this post.
On a 90-day certificate, renewing on day 60 leaves 30 days. If the renewal fails because port 80 was blocked, a DNS API token expired or the disk was full, you have a month of daily expiry emails to notice.
On a 64-day certificate, renewing on day 60 leaves four days. One failed run and the next attempt is two months away. The certificate expires 56 days before your script tries again.
It's worse than four days, because */2 in the month field doesn't mean "every 60 days." It fires on the first of January, March, May, July, September and November. July 1 to September 1 is 62 days. That leaves two.
On a 45-day certificate, from February 16, 2028, there is no margin to talk about. The certificate expires on day 45 and the job runs on day 60.
There is a quieter failure too, if your expiry alert fires when a certificate has fewer than 30 days left. Let's Encrypt's own rate-limit post from February 2026 says renewal moves from about day 60 of a 90-day certificate to about day 30 of a 45-day one. A 45-day certificate has fewer than 30 days left from day 15, so that alert fires on every healthy certificate from day 15 until it renews around day 30, half the time each one is in service. People mute noisy alerts. Size the threshold as a fraction of the lifetime instead.
How your client decides when to renew
Most current ACME clients never used a fixed interval. They look at the certificate they hold and renew relative to its expiry.
certbot. The certbot docs give a cron entry that runs certbot renew at midnight and noon after a random sleep, and most installs ship a default cron job or systemd timer for the same purpose. Each run only renews certificates that are due. Since certbot 4.0.0, "due" means less than one third of the lifetime remains; before that it was a hardcoded 30 days before expiry. Since 4.1.0 (June 2025), certbot renew also checks ARI, which for Let's Encrypt "will typically cause renewal at around 2/3rds of the certificate's lifetime," per the 4.1.0 release notes. Old certbot without ARI still copes with 64- and 45-day certificates, since 30 days before expiry lands on day 34 or day 15.
What breaks certbot setups is the wrapper: a cron that runs certbot certonly or certbot renew --force-renewal every 60 days instead of letting certbot renew decide.
Caddy. Caddy scans managed certificates every 10 minutes by default and renews when a third of the lifetime remains (renewal_window_ratio, default 0.3333). The same docs say that ratio is a suggestion, because an ACME issuer that implements ARI can set the renewal window instead. If you never touched those options, Caddy needs nothing from you.
Traefik. The ACME resolver docs say Traefik "manages 90-day certificates and starts renewing them 30 days before their expiry," with certificatesDuration "exclusively used to determine renewal dates." Thirty days before expiry still falls inside a 64-day or 45-day lifetime. The docs don't mention ARI, and a Traefik community thread asking about it had no confirmation of support as of May 2026. I haven't run Traefik against 64-day staging certificates myself, so test it before you trust that reading.
cert-manager. By default, cert-manager renews "2/3 through the X.509 certificate's duration", computed from the certificate actually issued. The trap is a fixed spec.renewBefore sized for 90-day certificates. A value like 1100h is longer than a 45-day certificate's whole life. The docs recommend spec.renewBeforePercentage "to prevent renewal loops." cert-manager 1.21 added "experimental support for RFC 9773 behind the ACMEUseARI feature gate."
acme.sh is worth a line too: its README says it follows the CA's ARI window with "no flag needed, no opt-in."
ARI in one paragraph
ACME Renewal Information is RFC 9773, published June 2025. The CA's directory advertises a renewalInfo URL. Your client sends an unauthenticated GET for its certificate (identified by the issuer's key ID and the serial number) and gets back a suggestedWindow with a start and an end. The RFC advises the client to pick a uniformly random time in that window, renew then, and poll again after the server's Retry-After. Two things follow. When the lifetime drops, the CA moves the window and your client follows without a config change. And if the CA has to revoke certificates early, it can pull the window forward, which a hardcoded interval can never react to. Let's Encrypt's rate-limit post adds that renewals are exempt from the new-certificate limits, so renewing more often costs you nothing there.
Find every certificate before February
You can't fix crons you don't know about. Start with the machines you remember.
# certbot: every lineage and its real expiry
sudo certbot certificates
# who schedules renewals on this box
systemctl list-timers | grep -i -E 'certbot|acme'
sudo grep -rn -E 'certbot|acme|renew' \
/etc/crontab /etc/cron.d /var/spool/cron 2>/dev/null
Then do the grep Let's Encrypt suggests, in your infra repo and runbooks:
grep -rnw -E '60|80|83' . | grep -i -E 'renew|cert|days'
Next, check what is actually being served. This works the same for Caddy, Traefik, nginx or a load balancer, because it reads the certificate off the wire with OpenSSL:
for host in example.com api.example.com; do
end=$(echo | openssl s_client -connect "$host:443" \
-servername "$host" 2>/dev/null \
| openssl x509 -noout -enddate)
echo "$host $end"
done
-checkend 864000 instead of -enddate exits non-zero when the certificate expires within ten days, which is enough for a CI job or a monitor.

The certificates you forgot about are the ones that expire first.
On Kubernetes, cert-manager records both the expiry and the planned renewal time in each Certificate's status. Read them with kubectl:
kubectl get certificates -A -o custom-columns=\
NS:.metadata.namespace,NAME:.metadata.name,\
EXPIRES:.status.notAfter,RENEW:.status.renewalTime
Last, find the certificates nobody remembers. Every publicly trusted certificate is logged in Certificate Transparency, and crt.sh searches those logs. %25 is a URL-encoded % wildcard; jq flattens the JSON:
q='%25.example.com'
url="https://crt.sh/?q=${q}&output=json&exclude=expired"
curl -s "$url" \
| jq -r '.[] | [.not_after, .issuer_name, .name_value] | @tsv' \
| sort -u
Any hostname in that list that isn't in your inventory is a certificate renewed by something you haven't checked yet: an old staging box, a status page, a forgotten admin panel.
The fix: let the client decide, then prove it on staging
Delete every renewal schedule that counts days. Run your client's renewal command once or twice a day and let it decide whether anything is due, which is exactly what the packaged certbot timer already does. Upgrade to a client version that speaks ARI where you can: certbot 4.1 or later, current Caddy and acme.sh, cert-manager with ACMEUseARI once you are comfortable with an experimental gate. Where ARI is not available, Let's Encrypt's December 2025 post calls renewing at roughly two thirds of the lifetime acceptable behaviour.
Then prove it while it is still cheap. Staging was scheduled to switch to 64-day certificates on October 14; check the lifetime of the certificate you get back before you trust the test. For certbot, sudo certbot renew --dry-run runs a full renewal against staging without saving anything. For the others, point a test instance at https://acme-staging-v02.api.letsencrypt.org/directory, issue a certificate, and confirm the renewal time your client computes lands well before day 64. Fix your expiry alerts in the same pass. You have until February 10 for the first step and until February 16, 2028 for the one that breaks every 60-day cron outright.
If this was useful
Certificate renewal is the textbook example of infrastructure a small team sets up once and forgets until it breaks on a Sunday. Ship It covers that whole layer between localhost and production: hosting, deploys, CI/CD, auth, payments and the monitoring that tells you something expired before your users do. If this post sent you hunting through crontabs, the book is a checklist for everything else on that box.

Top comments (1)
Good heads-up for anyone running their own cert renewal. With the window tightening to 64 days, the safe move is to renew at ~30 days remaining rather than relying on the old 60-day cron, which leaves real margin for a failed deploy or a DNS hiccup. Worth wiring the renewal into your existing health-check so a stalled renewal pages someone instead of silently expiring. Thanks for the timeline breakdown.