Certbot Renew Cron: Monitor Renewals Before Certificates Expire
Contents
certbot renew is the command that renews Let's Encrypt certificates, and the snap and Linux distribution packages run it twice a day from a systemd timer or a cron job. Each run renews only the certificates close to expiry: those with less than a third of their lifetime left in certbot 4.0 and later, or less than 30 days in older versions (certbot docs). Every other run does nothing and exits 0. When renewal starts failing, nothing visible changes, because the current certificate keeps working until the day it expires and Let's Encrypt no longer emails a warning. The only proof the job works is a record of every run.
Behavior below is from certbot 5.8.0, the current release (releases, checked October 25, 2026). Debian 13 packages certbot 4.0.0 and Debian 12 packages 2.1.0 (Debian); Ubuntu 24.04 packages 2.9.0 (Launchpad). certbot --version shows yours.
Where Each Install Method Schedules certbot renew#
| Install method | Scheduler | When certbot renew runs |
|---|---|---|
| Snap | snap.certbot.renew.timer |
One random time in 00:00-11:55 and one in 12:00-23:55, local |
| Debian or Ubuntu package | certbot.timer, or /etc/cron.d/certbot without systemd |
00:00 and 12:00 local, each plus a random delay of up to 12 hours |
| pip | None until you add a cron entry | When your entry says |
- Snap. The snap declares
renewas a oneshot service withtimer: 00:00~24:00/2(snapcraft.yaml). Each time snapd writes the timer unit, for example when the snap is installed or refreshed, it picks a random time inside each 12-hour window and stores the two as fixedOnCalendar=lines, with noPersistent=(service_timer_gen.go, snapd 2.78). A run due while the machine was off is not made up. - Debian and Ubuntu.
certbot.timersetsOnCalendar=*-*-* 00,12:00:00,RandomizedDelaySec=43200, andPersistent=true;certbot.servicerunscertbot -q renew --no-random-sleep-on-renew(Debian 13 packaging; Debian 12 and Ubuntu 24.04 ship the same units). The cron entry begins withtest -x /usr/bin/certbot -a \! -d /run/systemd/system, so on a host booted with systemd it exits without renewing. Disable the timer and nothing renews, even though/etc/cron.d/certbotis still there. - pip. Certbot's pip instructions add an
/etc/crontabline that runs at 00:00 and 12:00 after a random sleep of up to an hour.
When stdin isn't a terminal, certbot also waits a random 1 to 480 seconds before the first renewal of a run, unless --no-random-sleep-on-renew is set, as Debian's units do because the timer already randomizes (renewal.py). Since certbot 4.1, a CA's ACME Renewal Information (ARI) can move renewal earlier; for Let's Encrypt that typically means about two-thirds of the way through the lifetime (changelog).
certbot renew exits 1 when any certificate fails to renew or any renewal configuration can't be parsed, and 0 otherwise, including when nothing was due (renewal.py). It only considers certificates with a *.conf file in /etc/letsencrypt/renewal/ (storage.py) that don't set autorenew = False (storage.py).
Why Certbot Renewals Fail Without Anyone Noticing#
Each failure below leaves the working certificate in place, so the first symptom anyone sees is a browser error on expiry day, weeks after the first failed run.
- HTTP-01 can't reach port 80. The challenge "can only be done on port 80", redirects are followed only to ports 80 and 443, and Let's Encrypt may fetch the file from several vantage points (challenge types). A firewall rule, a CDN or load balancer in front of port 80, or a new redirect breaks it. A name with an AAAA record is checked over IPv6 first, with a fallback to IPv4 only on a connection timeout (IPv6 support), so an IPv6 address that answers from the wrong server fails every time.
- Standalone can't bind port 80 because the web server holds it. Pre and post hooks, below, stop and start the server.
- DNS-01 credentials stop working. A DNS plugin authenticates with the API token in its credentials file; once that token is revoked, expires, or loses access to the zone, every renewal fails.
--manualcertificates don't renew automatically unless they were set up with authentication hook scripts (certbot docs).- Rate limits. Only renewals that name the certificate they replace through ARI are exempt from every limit, and certbot 5.8.0's order request has no
replacesfield (acme/client.py). Its renewals still count toward 5 certificates per exact set of names every 7 days and 5 failed authorizations per name per hour (rate limits, updated August 5, 2026). Repeated--force-renewalwhile debugging can use up a week's allowance; test with--dry-run, which uses the staging server. - The server keeps the old certificate. With the
--nginxor--apacheplugin, certbot reloads the server after renewing (main.py). A certificate fromcertonly(webroot, standalone, DNS) is renewed on disk only, and nginx, HAProxy, or a mail server keeps serving the old one until it expires. A deploy hook that fails only logs a warning; certbot's exit status doesn't change (ops.py). - Nothing runs
certbot renew. The timer was disabled or masked, a container has neither systemd nor a cron daemon, a pip install never got its crontab line, or a renewal file was renamed out of*.conf. - A snap refresh changed something. snapd checks for updates four times a day by default (snap docs), so certbot can change between two unattended runs. For DNS plugins installed as separate snaps, certbot's instructions say that if certbot has issues, you may need to repeat
sudo snap set certbot trust-plugin-with-root=okand the plugin install.
Let's Encrypt Changes That Raise the Stakes#
Let's Encrypt stopped sending expiration notification emails on June 4, 2025, and deleted the addresses it had stored for them; it recommends a third-party monitoring service instead (Let's Encrypt). Nothing warns you before expiry now unless you set it up.
Lifetimes are shrinking too. As of October 25, 2026, the default classic profile issues 90-day certificates, the opt-in tlsserver profile 45-day ones, and shortlived 160-hour ones (profiles, updated October 4, 2026). The default drops to 64 days on February 10, 2027 (announcement) and to 45 days on February 16, 2028, according to the December 2025 timeline. Each step shortens the time between the first failed renewal and expiry:
| Certificate | Lifetime | Certbot 4.0+ starts renewing with |
|---|---|---|
classic until February 10, 2027 |
90 days | 30 days left |
classic from February 10, 2027 |
64 days | About 21 days left |
tlsserver, and classic from February 16, 2028 |
45 days | 15 days left |
shortlived |
160 hours | 80 hours left |
Certificates under 10 days renew at half their lifetime (renewal.py). Certbot before 4.0, as in Debian 12 and Ubuntu 24.04, renews 30 days before expiry whatever the lifetime.
Use Hooks to Stop, Start, and Reload Only When Needed#
certbot renew runs hooks only when a certificate is due, so they cost nothing on the runs that do nothing (certbot docs, cli help):
| Hook | Runs | Use it to |
|---|---|---|
--pre-hook, renewal-hooks/pre/ |
Before renewing, each distinct command once per run | Stop a server that holds port 80 |
--post-hook, renewal-hooks/post/ |
Once after all attempts, successful or not, with $RENEWED_DOMAINS and $FAILED_DOMAINS |
Start that server again |
--deploy-hook, renewal-hooks/deploy/ |
Once per renewed certificate, with $RENEWED_LINEAGE and $RENEWED_DOMAINS |
Reload the server or copy the new files |
The directories sit under /etc/letsencrypt/renewal-hooks/. Certbot runs every executable file in them, in byte order of their names, before any hook given on the command line or saved in a renewal configuration, so one file covers every certificate.
Reload the web server from a deploy hook, which runs only when there's a new certificate to load; a post hook also runs after failed attempts. This hook checks the configuration before reloading and leaves a marker when the reload fails, which the monitoring script below reports:
#!/bin/sh
# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx
# Runs once per renewed certificate; $RENEWED_LINEAGE is its live directory.
nginx -t -q && systemctl reload nginx && exit 0
echo "nginx reload failed after renewing $RENEWED_LINEAGE" >&2
touch /run/certbot-reload-failed
exit 1Test Renewal Before You Depend on It#
sudo certbot renew --dry-run # renew every certificate against staging
sudo certbot renew --dry-run --run-deploy-hooks # also run deploy hooks (certbot 2.3+)
sudo certbot certificates # names, paths, expiry, days left
systemctl list-timers 'certbot*' 'snap.certbot*' # next and last run
journalctl -u certbot.service --since "14 days ago" # snap: -u snap.certbot.renew.service--dry-run gets test certificates from the Let's Encrypt staging server without saving them. It runs pre and post hooks, and skips deploy hooks unless --run-deploy-hooks is given, which runs them against the current certificate (cli help). With the nginx or apache plugin, it makes temporary configuration changes, reloads the server, and rolls them back. It doesn't test the timer: sudo systemctl start certbot.service (or snap.certbot.renew.service) runs the real job once. certbot certificates prints each certificate's expiry as VALID: 37 days or INVALID: EXPIRED, and certbot keeps its logs in /var/log/letsencrypt.
After a renewal, compare what the server presents with the file on disk. Different dates mean nothing reloaded the server:
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -enddate
sudo openssl x509 -noout -enddate -in /etc/letsencrypt/live/example.com/cert.pemMonitor Every certbot renew Run#
CronRadar monitors the renewal job. It doesn't check certificate expiry, what your server presents, or whether the site is up; a certificate or uptime monitor does that, and cron monitoring vs uptime monitoring covers how the two fit together. There's no CronRadar SDK for certbot or systemd and no auto-discovery, so the renewal job reports with its own HTTP check-ins.
Report every run, not just the ones that renew. A run that renews nothing still proves the timer fired and certbot read every renewal configuration, and a check-in from every run is what lets a dead man's switch notice when the runs stop. This script reports both ways: from systemd hooks, or wrapped around the command in cron.
#!/bin/sh
# /usr/local/bin/certbot-cronradar: report certbot renew runs to CronRadar.
# systemd: "start" from ExecStartPre=, "finish" from ExecStopPost=.
# cron: "run" reports start, runs certbot renew, and reports its result.
url="https://cron.life/api/v1/ping/certbot-renew"
schedule="0 */12 * * *" # UTC: the timer's two daily windows on a UTC host
grace=46800 # the 12-hour random delay plus an hour
run_id=${INVOCATION_ID:-certbot-renew-$(date +%s)-$$}
[ -r /etc/cronradar.env ] && . /etc/cronradar.env # CRONRADAR_API_KEY=...
checkin() {
event=$1
shift
curl --fail --silent --show-error --max-time 5 --output /dev/null \
--request POST --get --user "$CRONRADAR_API_KEY:" \
--data-urlencode "schedule=$schedule" \
--data-urlencode "gracePeriod=$grace" \
--data-urlencode "run_id=$run_id" \
"$@" "$url/$event" || true
}
# Whole days until the soonest certificate on disk expires; 0 if none.
# Needs openssl and GNU date.
days_left() {
now=$(date +%s)
min=
for cert in /etc/letsencrypt/live/*/cert.pem; do
[ -f "$cert" ] || continue
end=$(openssl x509 -enddate -noout -in "$cert" | cut -d= -f2)
days=$(( ($(date -d "$end" +%s) - now) / 86400 ))
if [ -z "$min" ] || [ "$days" -lt "$min" ]; then
min=$days
fi
done
echo "${min:-0}"
}
finish() {
if [ "$1" != success ]; then
checkin fail --data-urlencode \
"message=certbot renew failed ($2); see /var/log/letsencrypt"
elif [ -e /run/certbot-reload-failed ]; then
checkin fail --data-urlencode \
"message=a deploy hook failed; reload the server, then delete /run/certbot-reload-failed"
else
curl --fail --silent --show-error --max-time 5 --output /dev/null \
--request POST --user "$CRONRADAR_API_KEY:" \
--header "Content-Type: application/json" \
--data "{\"outcomes\":{\"days_left\":$(days_left)}}" \
"$url/complete?run_id=$run_id" || true
fi
}
case $1 in
start) checkin start ;;
finish) finish "$SERVICE_RESULT" "systemd result $SERVICE_RESULT, ${EXIT_CODE:-} ${EXIT_STATUS:-}" ;;
run)
grace=4500 # cron: up to an hour of random sleep runs before this script
checkin start
timeout 1h certbot -q renew # a hang becomes exit code 124
rc=$?
if [ "$rc" -eq 0 ]; then finish success; else finish failed "exit code $rc"; fi
exit "$rc"
;;
esac
exit 0Attach it to the packaged service with a drop-in, leaving the unit file alone. For the snap, use the directory /etc/systemd/system/snap.certbot.renew.service.d/, and after a refresh check that systemctl cat snap.certbot.renew.service still lists it.
# /etc/systemd/system/certbot.service.d/cronradar.conf
[Service]
ExecStartPre=-/usr/local/bin/certbot-cronradar start
ExecStopPost=-/usr/local/bin/certbot-cronradar finish
TimeoutStartSec=1hsudo install -m 0755 certbot-cronradar /usr/local/bin/certbot-cronradar
sudo install -m 0600 /dev/null /etc/cronradar.env
sudoedit /etc/cronradar.env # one line: CRONRADAR_API_KEY=ck_app_12345_abcdef
sudo install -D -m 0644 cronradar.conf /etc/systemd/system/certbot.service.d/cronradar.conf
sudo systemctl daemon-reload
sudo systemctl start certbot.service # one real run; its check-ins create the monitor- Monitoring can't break renewal. Each request gives up after five seconds and ignores errors, and the
-prefixes tell systemd to ignore the script's own failures; systemd timers explains that pattern and$SERVICE_RESULT. The key file is root-only, and a missing one only makes the check-ins fail. - Results.
successsendscompletewith the days left. Any other result, such asexit-codeortimeout, sendsfail, which opens an incident immediately. A failed reload sendsfailtoo, and the marker keeps every later run failing until you delete it or reboot, which empties/run(file-hierarchy(7)). - Hangs. A oneshot service has no start timeout by default (systemd.service(5)), so a hung certbot would block every later run.
TimeoutStartSec=1hmakes systemd kill it and reporttimeout; set it well above your slowest run, since some DNS plugins wait minutes for propagation. - The monitor. The first
startcreates it with the schedule and grace period. Later check-ins don't change those, so edit them in the monitor's settings. Give each server its own monitor key inurl, such ascertbot-renew-web1, and use one CronRadar application and API key per environment.
Choosing the Schedule and Grace Period#
With a random delay, nothing says when a run is due, only which 12-hour window it falls in. The script sets the schedule to the window starts, 0 */12 * * *, and a grace period of 46,800 seconds: the 12-hour delay, the up-to-a-minute AccuracySec= default (systemd.timer(5)), and margin. The snap's times fall inside the same windows, so the same values work.
The trade-off is that the monitor sits in warning from the start of each window until that window's run reports, on average half of each window with the Debian timer's evenly distributed delay. warning is a status, not an alert. A window that ends without a run goes critical about an hour after it closes, so a stopped timer is flagged within about 25 hours of the last good run, well inside a 15- or 30-day margin. Choosing a grace period covers sizing it for other jobs.
CronRadar reads schedules in UTC, and the timer's windows open at local midnight and noon. On a host at UTC-5 without daylight saving, the schedule is 0 5,17 * * *; in a zone with daylight saving, use the earlier of the two UTC times and add an hour to the grace. Edit schedule and grace in the script before its first run.
Reporting Days Until Expiry#
days_left catches what the exit code can't: a certificate under /etc/letsencrypt/live that certbot no longer renews, because its renewal file was removed or renamed, or because it sets autorenew = False. Retire a certificate with certbot delete --cert-name rather than by deleting its renewal file, or the check will flag it. In the automation's settings in CronRadar, under Outcome checks, add days_left >= 14. That suits 90- and 64-day certificates; for 45-day ones, which certbot renews with about 15 days left, use 7. A missing or violated value opens an incident. Scheduled job silent success covers outcome checks in general, and the API reference the request rules.
This reads the files on disk, not what the server presents. A server that was never reloaded passes it while serving an older certificate, which is why the deploy hook marker exists and why a served-certificate check belongs in a certificate or uptime monitor.
pip Installs: Report From Cron#
Replace the /etc/crontab line from certbot's pip instructions with one that keeps the random sleep and runs the script:
0 0,12 * * * root /opt/certbot/bin/python -c 'import random; import time; time.sleep(random.random() * 3600)' && /usr/local/bin/certbot-cronradar runIn run mode the grace is 4,500 seconds, enough for the hour of sleep; GNU timeout turns a hang into a fail, and the script exits with certbot's own status. The same UTC rule applies to the schedule.
Frequently Asked Questions#
How do I check if certbot auto-renewal is working?#
Run sudo certbot renew --dry-run to test a full renewal against the staging server, systemctl list-timers to see the timer's last and next run, and sudo certbot certificates for days left. Those show the state today; a monitor that receives a check-in from every run shows when renewal breaks.
Does certbot renew automatically?#
Yes for the snap and the Debian and Ubuntu packages, which install a timer that runs certbot renew twice a day. A pip install needs a cron entry you add yourself. Each run renews only certificates near expiry, so most runs do nothing.
Why did my Let's Encrypt certificate expire?#
Either renewal had been failing for weeks while the old certificate still worked, for example because port 80 was blocked, a DNS API token expired, or the timer stopped, or the renewal succeeded on disk but the server was never reloaded. Compare certbot certificates with what openssl s_client shows for the site.
How do I reload nginx after certbot renews?#
Put an executable script that runs nginx -t && systemctl reload nginx in /etc/letsencrypt/renewal-hooks/deploy/. Certbot runs it once per renewed certificate and never on runs that renew nothing. Certificates obtained with --nginx are reloaded by certbot itself.
Do I still get Let's Encrypt expiration emails?#
No. Let's Encrypt ended expiration notification emails on June 4, 2025. It suggests a third-party certificate monitoring service; to know the renewal job itself still runs, monitor the job.
Put this guide to work
Know when the next run is missing
Connect one scheduled job, confirm a real completion, then verify the alert destination. Keep your existing scheduler in charge of running the work.