Guides Certificates

Site still shows the old SSL certificate

Updated 7 min read

Short answer

The server is still using the certificate it loaded before the renewal, or a different server is answering. Renewing writes new files; nginx, Apache and HAProxy keep serving the old certificate until they reload.

What to do: check what each address serves, reload the server that still has the old one, and add a deploy hook so the next renewal reloads it for you.

The SSL certificate checker connects from our server, not your browser, to each address the domain has (up to 4 per IP version), and shows what each serves. Free, no account. Check a certificate

Check what each address serves

A hostname often has several addresses: A records for IPv4 and AAAA records for IPv6, sometimes more than one of each. Visitors can land on any of them, and clients that follow Happy Eyeballs prefer IPv6 when both work (RFC 8305). So ask every address which certificate it serves:

host=example.com
for ip in $(dig +short A "$host" | grep -E '^[0-9.]+$') \
          $(dig +short AAAA "$host" | grep ':'); do
  printf '%-26s ' "$ip"
  echo | openssl s_client -connect "[$ip]:443" -servername "$host" \
      2>/dev/null \
    | openssl x509 -noout -serial -enddate | paste -s -d ' ' -
done
104.20.23.154              serial=01EEE6AABB521D5E14FC315FD9985690 notAfter=Dec 25 22:56:35 2026 GMT
172.66.147.243             serial=01EEE6AABB521D5E14FC315FD9985690 notAfter=Dec 25 22:56:35 2026 GMT
2606:4700:10::ac42:93f3    serial=01EEE6AABB521D5E14FC315FD9985690 notAfter=Dec 25 22:56:35 2026 GMT
2606:4700:10::6814:179a    serial=01EEE6AABB521D5E14FC315FD9985690 notAfter=Dec 25 22:56:35 2026 GMT

Run on October 23, 2026 with OpenSSL 3.6.4 and dig 9.10.6. example.com is healthy: four addresses, one certificate. On a site with the problem, one or more lines show an older serial number and an earlier notAfter.

-servername matters. It sends the hostname in the handshake (SNI), so a server that hosts several sites at one address knows which certificate to send (RFC 6066, section 3). Leave it out and you may see the server’s default certificate instead of the site’s.

Then compare with the file the server is configured to use, on the server itself. It prints the same two fields:

sudo openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem \
  -noout -serial -enddate

If an address serves a different serial number from the file, the server at that address hasn’t loaded it. If every address serves the new certificate but your browser shows the old one, skip to when only your browser shows the old one.

Six causes

Cause How it shows Fix
The server wasn’t reloaded Every address serves the old certificate; the file on disk is new. Reload it.
The reload failed The config test reports an error. nginx keeps the old configuration, and Apache refuses to restart. Fix the error, then reload.
The server reads another file The file at the configured path is old: a copy, or a fixed file under archive/. Point the config at /etc/letsencrypt/live/<name>/.
Another server or address answers One address in the check differs from the rest, such as an IPv6 address on another machine. Install the certificate there too, or fix the DNS record.
A CDN or load balancer has its own copy The addresses belong to the CDN, and its certificate didn’t change. Update it where TLS ends.
The browser kept its connection The check shows the new certificate, the browser the old one. Start a fresh connection.

The third cause is easy to miss. Certbot stores the actual files under /etc/letsencrypt/archive/ and updates the files in live/ to the newest at each renewal. Its documentation asks you to “point your (web) server configuration directly to those files” rather than copy them: a copy, or a path into archive/, stays old forever (Where are my certificates?).

Reload the web server

A reload makes the server read its configuration again, certificate files included. Test the configuration first, so an error shows up now instead of as a reload that quietly didn’t happen.

nginx:

sudo nginx -t && sudo systemctl reload nginx

Without systemd, sudo nginx -s reload does the same. On reload, nginx checks the new configuration, starts new worker processes with it, and lets the old ones finish their current requests. If the configuration is invalid, “the master process rolls back the changes and continues to work with the old configuration” (nginx beginner’s guide), which is how a failed reload keeps the old certificate.

Apache:

sudo apachectl configtest && sudo apachectl -k graceful

A graceful restart re-reads the configuration files and replaces each child process once it finishes its current request. Apache runs a syntax check first, and if it fails, “the server will refuse to restart” (Stopping and Restarting Apache HTTP Server).

HAProxy:

sudo systemctl reload haproxy

HAProxy reads the certificate files named in its configuration when it starts or reloads, and serves them from memory, so a renewed file on disk changes nothing until the next reload (configuration manual, crt). Its crt setting usually names one PEM file holding both the certificate and the key, built “by concatenating multiple PEM files into one”, so after a renewal that combined file has to be rebuilt before the reload; the hook below does it. HAProxy can also swap a certificate in memory without a reload, through its runtime API’s set ssl cert and commit ssl cert commands (management guide).

Make the next renewal reload it

Whether Certbot reloads anything depends on how you got the certificate:

  • With --nginx or --apache, Certbot reloads that server after each renewal. Its source says so: “In case of a renewal, reload server to pick up new certificate” (renew_cert in Certbot’s main.py).
  • With certonly (the webroot, standalone and DNS plugins), Certbot only writes files. Nothing reloads unless you add a hook.

A deploy hook runs “once for each successfully issued certificate” (Certbot’s command-line options). The simplest is an executable file in /etc/letsencrypt/renewal-hooks/deploy/, which Certbot runs after every successful renewal:

sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'EOF'
#!/bin/sh
nginx -t && systemctl reload nginx
EOF
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

For one certificate only, save the hook in that certificate’s renewal settings instead:

sudo certbot reconfigure --cert-name example.com \
  --deploy-hook "systemctl reload nginx"

Certbot first tests the change with a renewal against the staging server, then stores it in /etc/letsencrypt/renewal/example.com.conf, where it appears as renew_hook, the option’s older name (Certbot’s storage.py).

For HAProxy, save a hook like this one in the same deploy directory, executable. It rebuilds the combined file before the reload. Certbot sets $RENEWED_LINEAGE to the certificate’s live/ directory, and umask 077 keeps the new file, which holds the private key, readable by root only:

#!/bin/sh
umask 077
cat "$RENEWED_LINEAGE/fullchain.pem" "$RENEWED_LINEAGE/privkey.pem" \
  > "/etc/haproxy/certs/$(basename "$RENEWED_LINEAGE").pem"
systemctl reload haproxy

Test it with a dry run. A plain --dry-run skips deploy hooks; with --run-deploy-hooks, Certbot runs them “only if the dry run succeeds”, against your current certificate:

sudo certbot renew --dry-run --run-deploy-hooks

If the dry run itself fails, Certbot renew failed matches the error to its cause.

When a CDN or load balancer is in front

If dig returns your CDN’s or cloud load balancer’s addresses, visitors never see your server’s certificate. Cloudflare, for example, presents its own edge certificate to visitors, and your server’s certificate only secures the hop between Cloudflare and your server (Cloudflare’s SSL/TLS concepts). Renewing on your server changes that inner certificate only.

  • The CDN manages the edge certificate: your renewal doesn’t affect what visitors see. If the edge certificate is old, look at the CDN’s dashboard.
  • You uploaded a certificate to the CDN or load balancer: upload the renewed one there too, after every renewal, or automate it in the deploy hook.
  • To check your own server behind it, connect to its address directly with the same openssl s_client command, keeping -servername.

When only your browser shows the old one

If every address serves the new certificate, the server is fine and the browser is most likely showing an old connection:

  • Open connections stay open. “HTTP/2 connections are persistent” (RFC 9113, section 9.1), and a browser keeps using one, with the certificate it got at the start, until it closes.
  • Resumed sessions skip the certificate. When a browser resumes an earlier TLS session, the server “does not send a Certificate” (RFC 8446, section 2.2), so the browser shows the one from before.

Close every tab on the site or restart the browser, then look again. The command-line check above opens a new connection every time, which makes it the better judge.

Sources

All sources were checked on October 23, 2026. The command output comes from our own run on that date.

Keep reading