Bali, Indonesia
Insights

How to Confirm Certbot's systemd Timer Will Actually Renew Your Certificate

October 22, 2026
0:00
You set up Let's Encrypt with Certbot months ago, the site has worked ever since, and now someone asks if renewal is actually automated. You are fairly sure it is. You remember something about a systemd timer. But fairly sure is not the same as checked, and an expired certificate shows up as a browser warning in front of every visitor. Below are the questions that usually come up, roughly in the order you should check them.
A systemd timer is a unit that starts another unit on a schedule. It does the same job as a cron entry, but systemd records when it last ran and when it will run next. When Certbot comes from the Debian or Ubuntu package, the package normally installs certbot.timer, which starts certbot.service, which runs certbot renew. The snap version uses different names: snap.certbot.renew.timer and snap.certbot.renew.service. If Certbot was installed with pip into a virtualenv, there is often no timer at all. Plenty of people assume one exists anyway. Run systemctl list-timers --all and look for a Certbot entry. The NEXT and LEFT columns tell you when it fires next. LAST and PASSED tell you when it last ran. An empty LAST column, or a LAST date from months ago, tells you something before you dig any further. If nothing Certbot-related appears, check /etc/cron.d/ and root's crontab. Some packages ship a cron file as a fallback, and that file is usually written to step aside when systemd is running. Read it rather than guessing which mechanism you rely on. Also confirm the timer is enabled, not just present. Use systemctl is-enabled certbot.timer, or the snap unit name. A timer that was disabled during some earlier troubleshooting session looks fine at first glance, but it never fires.
Not necessarily, and this is where most of the confusion sits. The timer runs certbot renew often, typically twice a day, but certbot renew only replaces a certificate once it falls inside the renewal window. By default that means within 30 days of expiry. Newer Certbot versions can also take a renewal hint from the certificate authority. On most runs, Certbot looks at each certificate, decides it is not due yet, and exits cleanly. So a successful timer run usually means nothing was renewed, and that is expected behavior. The consequence is that a broken renewal path can stay hidden for weeks. Certbot reports success on every run because there is nothing to do. The real attempt happens only when the window opens. If the DNS has changed since installation, port 80 is now blocked, or the webroot path moved during a migration, that is the run that fails. To see what Certbot decided, read the service journal with journalctl -u certbot.service. Lines saying a certificate is not yet due for renewal are normal. Anything mentioning a failed challenge or an error deserves attention. sudo certbot certificates gives you the other half of the picture. It lists each certificate Certbot manages, the domains it covers, the expiry date, and the file paths. If the expiry date is already inside the 30-day window and the certificate still has not renewed, the timer is firing but the renewal itself is failing.
certbot renew --dry-run performs a full renewal for every managed certificate against the Let's Encrypt staging server. Staging is a test environment that issues untrusted certificates and does not count toward the production rate limits. Nothing is saved over your real certificate. That makes it safe to run at any time, and it is the fastest way to answer one question: can Certbot complete the domain validation challenge right now, with the current DNS, firewall, and web server config? It does not prove everything. In recent Certbot versions, a dry run skips deploy hooks by default unless you add --run-deploy-hooks. Pre and post hooks still run. So if your setup relies on a deploy hook to reload Nginx or restart a mail server, a clean dry run tells you nothing about that hook. It also runs in your shell, as you, rather than inside the systemd service environment. That difference rarely matters, but it can show up when a hook script depends on a PATH or an environment variable that the service does not have. When the dry run fails, the error usually names the domain and the challenge type. For HTTP validation, the common causes are DNS pointing somewhere else (sometimes only the AAAA record), a redirect or rewrite rule catching /.well-known/acme-challenge/, or a firewall that blocks port 80 because only HTTPS seemed necessary. In support work these failures often trace back to changes made long after Certbot was installed, by someone who had no reason to think about renewal.
Certbot writes new files under /etc/letsencrypt/live/yourdomain/, which are symlinks into the archive directory. Nginx and Apache read those files at startup or on reload, not on every connection. Until the web server reloads, it keeps serving the certificate it loaded earlier, even though a newer one is sitting on disk. Whether that reload happens depends on how the certificate was originally requested. Open /etc/letsencrypt/renewal/yourdomain.conf and look at the authenticator and installer lines. If the installer is nginx or apache, Certbot handles the reload itself after a successful renewal. If you used --webroot or --standalone with certonly, there is no installer. Certbot renews the files and stops there. Then you need a deploy hook: either a renew_hook line in that renewal config, or an executable script in /etc/letsencrypt/renewal-hooks/deploy/ that reloads your web server. Deploy hooks run only when a certificate was actually renewed, so they do not reload anything on the routine no-op runs. The same gap applies to anything else that uses the certificate, such as a mail server, an FTP daemon, or a reverse proxy in another container. Each one needs its own reload, and none of them happen on their own.
There are two logs. The systemd journal records each timer-triggered run. /var/log/letsencrypt/letsencrypt.log holds Certbot's own detailed log, including the challenge requests, the hooks it ran, and their exit status. Older runs are rotated into numbered files in the same directory. When a hook fails, the detail usually shows up here rather than in the short journal summary. The final check happens from the outside: which certificate does the server actually present to a client? Comparing that expiry date with certbot certificates tells you whether the reload step is working. Here are the checks together. All of them are read-only except the dry run, which is safe because it only talks to staging. Replace example.com with your domain and use the snap unit names if that is how Certbot was installed.
BASH
# Is a renewal timer scheduled, enabled, and has it run recently?
systemctl list-timers --all | grep -i certbot
systemctl is-enabled certbot.timer

# What did recent runs decide?
journalctl -u certbot.service --since '-7 days'

# Expiry dates and paths Certbot knows about
sudo certbot certificates

# Can renewal complete right now? Uses the staging server, saves nothing
sudo certbot renew --dry-run

# Which certificate is the live site actually serving?
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -enddate
If certbot certificates shows a later expiry than the openssl output, renewal is working but the reload is not. That points straight at the installer or deploy hook question above. If both dates match and are recent, the whole chain worked the last time it was needed.
Automatic renewal is a chain of four links: a timer that fires, a challenge that passes, a renewal window that opens, and a reload that picks up the new file. Most of the time a single link breaks quietly while everything else reports success. Run the dry run after any change to DNS, the firewall, or the web server config, not only once at install time. When you set up Certbot with webroot or standalone mode, add the deploy hook on the same day. Once a month, compare the expiry date the server actually presents with the date Certbot believes. That comparison catches more problems than reading the timer status alone.