# Certbot renewal

Certbot installs a systemd timer or cron job that runs `certbot renew` twice a day, so renewal is automatic from the moment the first certificate is issued and no manual command is normally needed. `certbot renew` is deliberately a no-op until a certificate is within 30 days of expiry, which means a successful run that renews nothing is the expected result rather than a failure. Verify the whole path end to end with `certbot renew --dry-run`, which exercises renewal against the staging environment without consuming rate limits. The most common real failure is not renewal itself but the reload that should follow it: the certificate on disk is new, the running web server still holds the old one in memory, and the fix is a deploy hook in `/etc/letsencrypt/renewal-hooks/deploy/` that reloads the service.

Source: https://certguru.io/platforms/certbot/
Last updated: 2026-08-19
Commands verified against OpenSSL 3.5.7.

---

## Renewal is already automatic

Installing Certbot installs a scheduler. On a systemd distribution that is `certbot.timer`, on a
snap install `snap.certbot.renew.timer`, and elsewhere `/etc/cron.d/certbot`. Each runs
`certbot renew` twice a day at a randomised minute.

*Is the timer active?*

```bash
systemctl list-timers | grep -i certbot
```

Expected output:

```
Sat 2026-08-22 04:17:33 UTC  8h left  certbot.timer  certbot.service
```

> **Do not add your own cron job**
>
> Almost every third-party guide tells you to write one. If the timer above exists, a second
>   scheduler just means two `certbot renew` processes contending for the same lock file, and the
>   loser logs an error that looks like a renewal failure. Check before adding.

`certbot renew` renews nothing until a certificate is within 30 days of expiry. A run that reports
`No renewals were attempted` is working correctly.

## Check what is actually managed

*Every lineage, its expiry and its files*

```bash
sudo certbot certificates
```

Expected output:

```
Certificate Name: example.com
  Domains: example.com www.example.com
  Expiry Date: 2026-11-04 09:12:44+00:00 (VALID: 76 days)
  Certificate Path: /etc/letsencrypt/live/example.com/fullchain.pem
  Private Key Path: /etc/letsencrypt/live/example.com/privkey.pem
```

Confirm independently what the running server is serving, which is not always the same thing:

*When does the certificate on disk expire?*

```bash
openssl x509 -in certificate.crt -noout -enddate
```

*Will it survive the next 30 days?*

```bash
openssl x509 -in certificate.crt -noout -checkend 2592000
```

Expected output:

```
Certificate will not expire
```

That exits non-zero when the certificate will expire inside the window, which makes it usable
directly as a monitoring check.

## Test the whole path without spending anything

*A real renewal against the staging environment*

```bash
sudo certbot renew --dry-run
```

This performs an actual challenge, so it catches a firewalled port 80, expired DNS credentials, a
webroot that moved and a plugin that vanished in a distribution upgrade — the four things that
break renewal months after issuance. It consumes no rate limit.

## The failure that is not a renewal failure

The single most common report is "Certbot renewed the certificate but the site still serves the old
one". Nothing is wrong with renewal. Web servers read certificates into memory at startup, and a
new file on disk is invisible until the process reloads.

Certbot runs every executable in the deploy-hook directory **only when a certificate was actually
renewed**, which is exactly the right trigger:

*Reload nginx after any successful renewal*

```bash
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh >/dev/null  **deploy, not post**
>
> A `post` hook runs after every `certbot renew`, including the twice-daily runs that renew nothing
>   — so it reloads your web server 730 times a year for no reason. A `deploy` hook runs only when a
>   certificate changed. The renewed lineage is available to the script as `$RENEWED_LINEAGE`.

## When renewal genuinely fails

Read the log first; it names the challenge that failed and why:

```bash
sudo tail -50 /var/log/letsencrypt/letsencrypt.log
```

| Symptom in the log | Cause | Fix |
| --- | --- | --- |
| `Timeout during connect` on HTTP-01 | Port 80 closed or firewalled | Open port 80; the challenge needs it even on an HTTPS-only site |
| `404` fetching the challenge file | Webroot moved, or an alias intercepts `/.well-known/` | Correct `webroot_path` in the renewal config |
| `NXDOMAIN` / DNS-01 failures | DNS API credentials expired or rotated | Renew the credentials file named in the renewal config |
| `unauthorized` after a working year | The domain no longer resolves to this host | Point DNS back, or remove the lineage |
| `too many certificates already issued` | Duplicate-certificate rate limit | Wait; stop using `--force-renewal` |
| `Could not choose appropriate plugin` | Authenticator plugin removed by a distro upgrade | Reinstall the plugin package |

The renewal configuration is a plain file, and editing it is the supported way to change how a
lineage renews:

```bash
sudo cat /etc/letsencrypt/renewal/example.com.conf
```

## Point the server at the right file

Certbot writes four files per lineage. Which one you reference matters:

| File | Contents | Use it for |
| --- | --- | --- |
| `fullchain.pem` | Leaf **plus** intermediates | nginx `ssl_certificate`, Apache `SSLCertificateFile` |
| `cert.pem` | Leaf only | Almost nothing — produces an incomplete chain |
| `chain.pem` | Intermediates only | Legacy `SSLCertificateChainFile` on Apache  **Never copy the files out of live/**
>
> Everything in `/etc/letsencrypt/live/` is a symlink into `archive/`, and Certbot repoints those
>   symlinks on each renewal. A configuration pointing at a copy keeps working for 90 days and then
>   serves an expired certificate, with renewal logs that report success the whole time. Point at the
>   `live/` path.

Serving `cert.pem` is the other half of this trap: the site loads in a browser, which repairs the
missing intermediates itself, and fails for curl, mobile apps and monitoring with
[unable to get local issuer certificate](/errors/unable-to-get-local-issuer-certificate/).

## Wildcards renew differently

A wildcard lineage can only be validated over DNS-01, so its renewal depends on DNS API credentials
that no one thinks about until they expire. The renewal config records which plugin and credentials
file to use, and `--dry-run` is the only way to find out that they have stopped working before the
certificate does. See [wildcard certificates](/concepts/wildcard-certificate/) for why the challenge
type is forced.


## Frequently asked questions

### certbot renew says "No renewals were attempted". Is that a failure?

No, it is the normal result. Certbot only renews inside the last 30 days of a 90-day certificate, so most runs correctly do nothing. Use `certbot renew --dry-run` to exercise the path regardless of expiry.

### The certificate renewed but my site still serves the old one. Why?

nginx, Apache and HAProxy read certificates into memory at startup. A new file on disk changes nothing until the service reloads. Add a deploy hook that reloads it — that step is missing far more often than renewal itself fails.

### Do I need a cron job? Every guide adds one.

Almost certainly not. Package and snap installations both ship a timer that already runs renewal twice a day, and adding a second scheduler means two processes competing for the same lock. Check with `systemctl list-timers` before writing one.

### Should nginx point at cert.pem or fullchain.pem?

`fullchain.pem`. It contains the certificate followed by the intermediates; `cert.pem` is the leaf alone, and serving it produces an incomplete chain that browsers hide and every other client rejects.

### Can I copy the files out of /etc/letsencrypt/live/?

Point at them, do not copy them. The files there are symlinks into `archive/` that Certbot repoints on each renewal, so a copy is a snapshot that silently goes stale while the symlink stays current.

### What does --force-renewal cost?

It issues a new certificate regardless of expiry, which counts against the duplicate-certificate rate limit. Use it to recover from a genuinely broken certificate, not as a way to test renewal — `--dry-run` exists for that and is free.


## References

- [Certbot user guide](https://eff-certbot.readthedocs.io/en/stable/using.html) — renewal, hooks and the renewal configuration file
- [Let's Encrypt rate limits](https://letsencrypt.org/docs/rate-limits/) — the current published limits, including the duplicate-certificate limit
- [RFC 8555: ACME](https://www.rfc-editor.org/rfc/rfc8555) — the protocol Certbot speaks, and what a challenge actually proves

## Related pages

- https://certguru.io/concepts/free-tls-certificate/
- https://certguru.io/concepts/wildcard-certificate/
- https://certguru.io/errors/certificate-has-expired/
- https://certguru.io/platforms/nginx/
- https://certguru.io/openssl/check-certificate-expiry/
- https://certguru.io/concepts/certificate-chain/

--
CertGuru — https://certguru.io
