Guide Developer tools

TLS certificates drop to 47 days: the dates and what to automate

Public TLS certificates max out at 100 days from March 15, 2027 and 47 from 2029. Our simulation shows which renewal rules let them lapse.

The short read

A public TLS certificate issued from March 15, 2029 may last 47 days at most, down from 398 days.

  • The next cut, to 100 days, applies from March 15, 2027. Let's Encrypt moves its default from 90 to 64 days on February 10, 2027.
  • In our simulation, rules that renew at a fixed certificate age, such as 60 days, let a 47-day certificate expire.

What you can do with this

Search your cron jobs and timers for fixed renewal ages, move them to renew with a third of the lifetime left, and alert on a share of the lifetime instead of a fixed 30 days.

A dense tangle of blue, white and red network cables hanging down the front of a server rack.
Photo: Kim Scarborough, CC BY-SA 2.0
In this article
  1. The schedule, from the requirements’ own text
  2. Why the browsers pushed for it
  3. Let’s Encrypt moves ahead of the deadline
  4. Our test: which renewal rules survive 47 days
  5. Rules keyed to a certificate’s age fail first
  6. Step 1: list every public certificate you own
  7. Step 2: find hardcoded renewal ages
  8. Step 3: let the ACME client choose when to renew
  9. Step 4: make the new certificate reach the server
  10. Step 5: alert on a share of the lifetime, not on 30 days
  11. What the script did on our test certificates
  12. When automation is hard: paid certificates and appliances
  13. When not to use a public certificate at all
  14. The simulation code
  15. How we made this

Publicly trusted TLS certificates issued from March 15, 2027 may last 100 days at most, and from March 15, 2029 just 47, under the CA/Browser Forum’s Baseline Requirements. The first cut, to 200 days, has applied since March 15, 2026. A certificate someone renews by hand once a year becomes eight renewals a year, and the domain check behind each one shrinks to a 10-day reuse window.

If you run a website or an appliance with a public certificate, this guide is for you. It gives the dates from the rule text and a simulation of which renewal rules survive. It ends with a tested script that alerts before a certificate lapses. We read the requirements and the Let’s Encrypt and Certbot documents on October 27, 2026.

The schedule, from the requirements’ own text

The schedule comes from ballot SC-081, which passed on April 11, 2025. It was proposed by Clint Wilson of Apple and endorsed by Sectigo, Google Chrome and Mozilla, according to the ballot page. Certificate issuers voted 25 for, none against and 5 abstaining; all four browser makers that voted (Apple, Google, Microsoft and Mozilla) voted for it.

The dates below are copied from sections 4.2.1 and 6.3.2 of the Baseline Requirements, version 2.3.1, dated October 4, 2026.

Certificate issued Maximum validity Domain validation reuse Organization details reuse (OV)
Before March 15, 2026 398 days 398 days 825 days
March 15, 2026 to March 14, 2027 200 days 200 days 398 days
March 15, 2027 to March 14, 2029 100 days 100 days 398 days
From March 15, 2029 47 days 10 days 398 days

Three details in the text matter more than the headline number.

The limit follows the issue date. The tables are keyed to “Certificate issued on or after”, so a certificate already in use keeps its full term. A 200-day certificate issued on March 1, 2027 still runs into September.

Expect a day less than the maximum. Section 6.3.2 says certificates “SHOULD NOT” exceed 199, 99 and 46 days in the three periods. It counts a day as 86,400 seconds and treats any extra fraction as another day, so it tells CAs not to issue the full maximum by default.

Domain checks shrink faster than lifetimes. Each certificate needs proof that you control the domain. A CA may reuse that proof for 200 days now, 100 days from 2027 and 10 days from 2029. By 2029, nearly every renewal repeats the check. Verified organization details, which OV certificates carry, can still be reused for 398 days.

The rules cover publicly trusted certificates only. Section 1.1 says they apply to certificates trusted because their root ships “in widely-available application software”. A private CA for internal names does not have to follow them.

Why the browsers pushed for it

The ballot’s stated rationale is short: “Certificates are representations of a point in time state of reality.” A domain can change owners, and a key can leak, while a certificate keeps vouching for the old facts until it expires. A shorter term caps how long a wrong certificate stays valid.

The ballot page names wider use of two-way automation as a side benefit. That is the part that costs you work. When a renewal comes every few weeks, a reminder in someone’s calendar stops being a plan.

Here is the arithmetic, computed by us from the table. Renewing at the last possible moment, one certificate needs about 1 renewal a year at 398 days, 2 at 200 days, 4 at 100 days and 8 at 47 days. Renewing with a third of the lifetime left, as Certbot does by default, a 47-day certificate renews every 31 days or so, close to 12 times a year.

Let’s Encrypt moves ahead of the deadline

Let’s Encrypt, the free CA, shortens its own certificates before the rules force it. Its December 2025 announcement and its October 7, 2026 update give this schedule:

Date Default certificate Authorization reuse
Until February 9, 2027 90 days 30 days
From February 10, 2027 64 days 10 days
From February 16, 2028 45 days 7 hours

The staging environment switched to 64-day certificates on October 14, 2026, so you can test against it now. Let’s Encrypt expects the last 90-day certificate to expire on May 11, 2027, and says it will not revoke valid certificates as part of the change. Its profiles page lists a tlsserver profile that already issues 45-day certificates and a shortlived profile at 160 hours, about six days.

One safety net is already gone. Let’s Encrypt stopped sending expiration emails on June 4, 2025, citing its cost and privacy, and noting that more subscribers now automate renewal. If nobody set up monitoring after that, nobody hears about a failed renewal until the site breaks.

Our test: which renewal rules survive 47 days

Let’s Encrypt’s advice is to stop renewing on a fixed schedule. Its October post tells subscribers to renew at “approximately ⅔ of the lifetime”. It also says to “grep for common hardcoded numbers like 83, 80 or 60 in cron jobs, wrapper scripts and runbooks.”

We wanted to see how much margin each common rule leaves. So we wrote a short simulation (code below) of six renewal rules at six certificate lifetimes. For each pair it tries every start time of the scheduled job in one-hour steps and keeps the worst case. It reports the days left on the certificate at the first renewal attempt, and how many attempts fit before expiry if every attempt fails.

These are modeled schedules, not measurements of real servers. Each cell reads “days left at first attempt / attempts before expiry”.

Rule 398 d 200 d 100 d 90 d 64 d 47 d
Renew at age 60 days, checked daily 337 / 338 139 / 140 39 / 40 29 / 30 3 / 4 Expires
Renew at age 83 days, checked daily 314 / 315 116 / 117 16 / 17 6 / 7 Expires Expires
Renew under 30 days left, checked daily 29 / 30 29 / 30 29 / 30 29 / 30 29 / 30 29 / 30
Renew under 30 days left, checked monthly under 1 hour / 1 same same same same same
Renew under 1/3 of lifetime left, checked daily 132 / 132 66 / 66 32 / 33 29 / 30 20 / 21 15 / 15
Renew under 1/3 of lifetime left, checked weekly 126 / 18 60 / 9 26 / 4 23 / 4 14 / 3 9 / 2

Rules keyed to a certificate’s age fail first

Rules keyed to the certificate’s age are the trap. “Renew at 60 days” was fine for 90-day certificates. It leaves 3 days on a 64-day certificate and never fires on a 47-day one. “Renew at 83 days” already fails at 64 days, the Let’s Encrypt default from February 2027.

A job that checks monthly is unsafe at any lifetime. With a 30-day threshold, the worst start time leaves one attempt with under an hour to spare, so a single failure means an expired certificate. The “under 30 days left” rule checked daily keeps working at 47 days, but it renews after 17 days, more often than needed.

The rule Certbot uses since version 4.0, a third of the lifetime, scales with the certificate. Checked daily, it gives a 47-day certificate 15 attempts, about two weeks to notice and fix a failure.

Step 1: list every public certificate you own

Start with an inventory: the names, where each certificate is installed and what renews it. Look beyond web servers. Load balancers, mail servers, VPN gateways, printers with web consoles and vendor appliances often carry a certificate someone uploaded by hand.

Write down for each one whether a tool renews it (an ACME client, a hosting platform, a cloud load balancer) or a person does. Every manual entry is a job that runs eight or more times a year from 2029.

Step 2: find hardcoded renewal ages

Search scheduled jobs for fixed intervals near renewal commands. On a Linux server:

sudo grep -rnE '\b(30|60|80|83|90)\b' /etc/crontab /etc/cron.d /var/spool/cron 2>/dev/null | grep -iE 'cert|acme|renew|ssl'
systemctl list-timers --all | grep -iE 'certbot|acme|lego'

The first line lists cron entries that mention a certificate tool and a suspicious number. The second shows systemd timers for common ACME clients. Also search deployment scripts and runbooks in your repositories for the same numbers.

Replace anything that says “renew every N days” or “renew when the certificate is N days old” with a rule based on time left, as in step 3.

Step 3: let the ACME client choose when to renew

If you use Certbot, check its version with certbot --version. The changelog shows two changes that matter here:

  • Version 4.0.0 (April 7, 2025): certificates renew with a third of their lifetime left, or half for lifetimes under 10 days. Before that, the threshold was a fixed 30 days.
  • Version 4.1.0 (June 10, 2025): certbot renew checks ACME Renewal Information (ARI), a standard published as RFC 9773, and may renew early when the CA asks. For Let’s Encrypt certificates that typically means renewal at about two thirds of the lifetime.

The Certbot user guide says most installations come with a scheduled task that runs certbot renew, which you can find in the crontab or with systemctl list-timers. Check that a renewal configuration file under /etc/letsencrypt/renewal/ does not pin an old renew_before_expiry value. Without an ARI response, that setting still overrides Certbot’s default, according to the 4.1.0 changelog entry.

Test against Let’s Encrypt’s staging environment before February 10, 2027. With Let’s Encrypt certificates, certbot renew --dry-run uses staging, according to the changelog, so it now exercises 64-day certificates.

Other ACME clients differ. Let’s Encrypt says to check your client’s documentation for ARI support; if it has ARI, the CA can tell it when to renew.

Step 4: make the new certificate reach the server

A renewed certificate on disk does nothing until the service loads it. Certbot runs a --deploy-hook command only after a successful renewal, or any executable placed in /etc/letsencrypt/renewal-hooks/deploy/, per its user guide. Use it to reload nginx or Apache, or to copy the files to the load balancer or appliance that serves them.

This step fails quietly. The renewal log says success while the old certificate stays loaded in memory. A monitor that connects to the live service, like the script below, is the only way to catch it.

Step 5: alert on a share of the lifetime, not on 30 days

A fixed “alert under 30 days” breaks as lifetimes shrink. Take a 47-day certificate renewed with a third of its lifetime left. Even when healthy, it spends about two weeks of each cycle under 30 days, so the alert fires constantly and people learn to ignore it.

This script reads each certificate’s own start and end dates and alerts relative to its lifetime. It connects to a host, or reads a PEM file, and prints one line per target:

  • OK: more than a third of the lifetime left.
  • RENEW_DUE: under a third left, the window where ACME clients renew.
  • ALERT: under a quarter left (change it with --alert-fraction), so renewal should have happened days ago.
  • EXPIRED or ERROR: past its end date, or the target could not be read.

It also makes a second, normal TLS connection to check the chain and hostname, and notes when that fails. It exits with code 2 when anything needs attention, so a cron job or CI step can page someone. It needs Python 3 and pip install cryptography.

#!/usr/bin/env python3
"""Report days left on TLS certificates and alert relative to each one's lifetime.

Targets are host, host:port or a path to a PEM file. Exit code 0 when all are
fine, 2 when any is expired, close to expiry or could not be read.
Needs: pip install cryptography
"""
import argparse
import datetime as dt
import socket
import ssl
import sys

from cryptography import x509

UTC = dt.timezone.utc


def not_before(cert):
    # cryptography 42+ has the *_utc properties; older versions return naive UTC
    return getattr(cert, "not_valid_before_utc", None) or cert.not_valid_before.replace(tzinfo=UTC)


def not_after(cert):
    return getattr(cert, "not_valid_after_utc", None) or cert.not_valid_after.replace(tzinfo=UTC)


def fetch(target, timeout):
    """Return (certificate, trusted) for a host[:port] or a PEM file path."""
    if "/" in target or target.endswith((".pem", ".crt")):
        with open(target, "rb") as f:
            return x509.load_pem_x509_certificate(f.read()), None
    host, _, port = target.partition(":")
    port = int(port or 443)
    # First handshake without verification, so expired or untrusted
    # certificates can still be read.
    ctx = ssl.create_default_context()
    ctx.check_hostname = False
    ctx.verify_mode = ssl.CERT_NONE
    with socket.create_connection((host, port), timeout=timeout) as sock:
        with ctx.wrap_socket(sock, server_hostname=host) as tls:
            der = tls.getpeercert(binary_form=True)
    return x509.load_der_x509_certificate(der), verify(host, port, timeout)


def verify(host, port, timeout):
    """Second handshake with normal checks: chain, dates and hostname."""
    ctx = ssl.create_default_context(cafile=ARGS.cafile)
    try:
        with socket.create_connection((host, port), timeout=timeout) as sock:
            with ctx.wrap_socket(sock, server_hostname=host):
                return True
    except ssl.SSLCertVerificationError as e:
        return e.verify_message or "verification failed"


def main():
    targets = list(ARGS.targets)
    if ARGS.file:
        with open(ARGS.file) as f:
            targets += [l.strip() for l in f if l.strip() and not l.startswith("#")]
    if not targets:
        sys.exit("no targets: pass host[:port] or PEM paths, or --file list.txt")

    now = dt.datetime.now(UTC)
    bad = 0
    print("target\tstatus\tdays_left\tlifetime_days\texpires_utc\tnote")
    for t in targets:
        try:
            cert, trusted = fetch(t, ARGS.timeout)
        except Exception as e:  # one failing target must not stop the rest
            bad += 1
            print(f"{t}\tERROR\t\t\t\t{type(e).__name__}: {e}")
            continue
        start, end = not_before(cert), not_after(cert)
        life = (end - start).total_seconds() / 86400
        left = (end - now).total_seconds() / 86400
        if left <= 0:
            status = "EXPIRED"
        elif left < life * ARGS.alert_fraction:
            status = "ALERT"  # renewal was due days ago and has not happened
        elif left < life / 3:
            status = "RENEW_DUE"  # ACME clients normally renew in this window
        else:
            status = "OK"
        note = "" if trusted in (True, None) else f"untrusted: {trusted}"
        if status in ("EXPIRED", "ALERT") or note:
            bad += 1
        print(f"{t}\t{status}\t{left:.1f}\t{life:.0f}\t{end:%Y-%m-%d %H:%M}\t{note}")
    sys.exit(2 if bad else 0)


if __name__ == "__main__":
    p = argparse.ArgumentParser(description=__doc__)
    p.add_argument("targets", nargs="*", help="host, host:port or cert.pem")
    p.add_argument("--file", help="text file with one target per line")
    p.add_argument("--alert-fraction", type=float, default=0.25,
                   help="alert when less than this share of the lifetime is left (default 0.25)")
    p.add_argument("--cafile", help="extra CA bundle for the trust check (private CAs)")
    p.add_argument("--timeout", type=float, default=10)
    ARGS = p.parse_args()
    main()

Run it as python3 check_certs.py example.com mail.example.com:465, or put one target per line in a file and pass --file hosts.txt. Schedule it daily from a machine other than the one it checks, so it still runs when that server is down.

What the script did on our test certificates

We made a local test CA with OpenSSL on October 27, 2026 and issued six certificates for localhost with backdated start dates. They covered every status, from a fresh 398-day certificate to a 90-day one that had expired 2 days earlier. We served three of them with openssl s_server and pointed the script at the files, the three servers and one closed port.

Target Status Days left Lifetime
398-day file OK 398.0 398
200-day file OK 80.0 200
47-day file, 14 days left RENEW_DUE 14.0 47
47-day file, 9 days left ALERT 9.0 47
6-day file, 1 day left ALERT 1.0 6
90-day file, expired EXPIRED -2.0 90
Server with the 47-day certificate RENEW_DUE 14.0 47
Server with the expired certificate EXPIRED -2.0 90, noted “untrusted: certificate has expired”
Server with the 398-day certificate OK 398.0 398
Closed port ERROR ConnectionRefusedError

It exited with code 2, and the error on the closed port did not stop the other checks. Without --cafile, the server with our test CA’s certificate was flagged “unable to get local issuer certificate” and the exit code was 2. An empty target list stopped with a message instead of printing an empty report.

A fixed 30-day alert would have flagged the healthy 47-day certificate with 14 days left. The script marks it RENEW_DUE, which is the normal state while the ACME client works.

When automation is hard: paid certificates and appliances

Paid certificates get no exemption. An OV certificate’s organization check can be reused for 398 days. But the domain check falls to 10 days in 2029, so a portal that emails a validation link would send one for nearly every certificate.

The requirements offer a way to make the repeated check hands-off. Method 3.2.2.4.22, added by ballot SC-088 in 2025, lets you publish one standing TXT record at _validation-persist.<your domain> that names your account at the CA. A validation done this way counts for at most 10 days, so the CA checks the record again, but nobody has to change DNS for each renewal.

Let’s Encrypt wrote in February 2026 that it targeted production support for some time in the second quarter of 2026. Ask your own CA whether it supports the method.

The trade-off, in Let’s Encrypt’s words, is that protecting the ACME account key “becomes the central concern”.

If a device cannot run an ACME client, put a reverse proxy that runs one in front of it. Or check whether the vendor’s current firmware supports ACME.

When not to use a public certificate at all

A service that only your own staff and machines reach can use a private CA. The Baseline Requirements do not bind it, so you choose the lifetime. The price is distributing your root certificate to every device and container that connects, and keeping its key safe. For anything the public or customers reach, stay with a publicly trusted certificate and automate it.

Do not jump to six-day certificates just because they exist. Let’s Encrypt’s shortlived profile suits teams whose renewal already runs on ARI with monitoring. In our simulation, a weekly check with the one-third rule left a 47-day certificate only 2 attempts. A six-day certificate leaves far less room.

The simulation code

This is the script behind the renewal-rule table. It uses only the Python standard library.

"""Which renewal rules survive shorter certificate lifetimes?

For each rule and lifetime, try every start time of the job (in one-hour
steps) and record the worst case: days left on the certificate at the first
renewal attempt, and how many attempts fit before it expires.
"""
import math

LIFETIMES = [398, 200, 100, 90, 64, 47]

# name, days between job runs, function(age, remaining, lifetime) -> renew?
RULES = [
    ("Renew at age 60 days, checked daily", 1, lambda age, rem, life: age >= 60),
    ("Renew at age 83 days, checked daily", 1, lambda age, rem, life: age >= 83),
    ("Renew under 30 days left, checked daily", 1, lambda age, rem, life: rem < 30),
    ("Renew under 30 days left, checked monthly", 30, lambda age, rem, life: rem < 30),
    ("Renew under 1/3 of lifetime left, checked daily", 1, lambda age, rem, life: rem < life / 3),
    ("Renew under 1/3 of lifetime left, checked weekly", 7, lambda age, rem, life: rem < life / 3),
]

STEP = 1 / 24  # one hour, in days


def worst_case(period, rule, life):
    worst_left, worst_attempts = math.inf, math.inf
    for k in range(int(period / STEP)):
        phase = (k + 0.5) * STEP  # half-step offset avoids runs exactly at expiry
        # certificate issued at day 0; job runs at phase, phase + period, ...
        t = phase
        while t < life:
            age, rem = t, life - t
            if rule(age, rem, life):
                attempts = math.ceil(rem / period)  # runs at t, t+p, ... before expiry
                worst_left = min(worst_left, rem)
                worst_attempts = min(worst_attempts, attempts)
                break
            t += period
        else:
            # no run renewed before expiry
            return 0.0, 0
    return worst_left, worst_attempts


def main():
    print("rule\t" + "\t".join(f"{l}d" for l in LIFETIMES))
    for name, period, rule in RULES:
        cells = []
        for life in LIFETIMES:
            left, attempts = worst_case(period, rule, life)
            cells.append("EXPIRES" if attempts == 0 else f"{left:.2f}d/{attempts}")
        print(name + "\t" + "\t".join(cells))


if __name__ == "__main__":
    main()

Change LIFETIMES or add a rule that matches your own cron job to see its margin before February 10, 2027, when Let’s Encrypt’s 64-day default arrives.

For scheduled jobs beyond certificates, our guide to making scheduled scripts report failures covers heartbeats and alerts, and the eMudhra case shows what happens on the trust-store side when a CA fails.

How we made this

We read the Baseline Requirements (version 2.3.1), the SC-081 ballot page, Let’s Encrypt’s announcements and profiles page, and Certbot’s user guide and changelog on October 27, 2026. We wrote and ran the renewal simulation, which models schedules rather than measuring real servers. We tested the expiry script on certificates from a local OpenSSL test CA, served on localhost. We did not test against public websites, because the sandbox’s network proxy replaces their certificates, and we did not try any paid CA’s portal or a vendor appliance.

Sources