Why Your Let's Encrypt Certificate Stopped Renewing — The Six Failures I Fix on WordPress Servers

· 12 min read

A client messaged me on a Tuesday morning: "Google is showing a security warning on our site." I checked the domain and sure enough, the SSL certificate had expired three days earlier. Their WooCommerce store had been showing a full-page browser warning to every visitor since Saturday. Three days of lost trust and abandoned carts, and nobody noticed until a customer screenshot landed in the inbox.

The server was a self-managed VPS running nginx and Certbot. The same setup I run on dozens of client servers. Let's Encrypt certificates are supposed to renew automatically — that's the entire point. But "supposed to" and "actually does" are different things when you're managing real infrastructure.

I've fixed this exact scenario more times than I'd like to admit across the 70+ sites I manage. The certificate was always "going to renew itself." Until it didn't. Here are the six causes I keep finding.

1. Nginx Is Blocking the ACME Challenge

This is the most common cause by a wide margin. Let's Encrypt's HTTP-01 validation works by placing a token file at /.well-known/acme-challenge/ and then requesting it over port 80. If nginx can't serve that file, the challenge fails and the certificate doesn't renew.

The usual culprit is a catch-all redirect that sends everything to HTTPS before the challenge file can be served:

server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

That redirect applies to every request on port 80 — including the ACME challenge. Let's Encrypt will follow the redirect to HTTPS, and in theory that can work. But in practice it often fails: if the existing certificate has already expired, the TLS handshake on the redirect target fails and the challenge is rejected. Even when the redirect does work, it adds a dependency on the HTTPS config being correct — which is exactly the thing you're trying to fix. Exempting the challenge path removes the ambiguity entirely:

server {
    listen 80;
    server_name example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

I also check for rules that block dotfiles or hidden directories. Some security-hardened configs include a blanket block on anything starting with a dot:

location ~ /\. {
    deny all;
}

That kills .well-known too. The fix is to add an exception above it:

location ^~ /.well-known/ {
    allow all;
}

After any nginx change, always test and reload:

sudo nginx -t && sudo systemctl reload nginx

2. Cloudflare Is Redirecting the Challenge Away

If the domain sits behind Cloudflare with the proxy enabled (orange cloud), HTTP-01 challenges can break in two ways.

First, Cloudflare's "Always Use HTTPS" setting (under SSL/TLS > Edge Certificates) redirects all HTTP traffic to HTTPS at the edge. Let's Encrypt will follow the redirect, but the response comes from Cloudflare's edge rather than your origin server, which can serve a cached or incorrect challenge token. When combined with an expired origin certificate or Cloudflare's own TLS termination, this chain of redirects frequently causes validation failures.

Second, Cloudflare's caching and WAF rules can interfere. I've seen cached ACME challenge responses from previous renewal attempts being served to the validator, which obviously fails.

The cleanest fix is to create a Cloudflare configuration rule that disables HTTPS redirection for the challenge path:

  • URL match: example.com/.well-known/acme-challenge/*
  • Settings: Disable "Always Use HTTPS", disable "Automatic HTTPS Rewrites", set SSL mode to "Off" for this path

The alternative — and what I now use on most Cloudflare-proxied servers — is to switch to DNS-01 validation instead of HTTP-01. With the Cloudflare DNS plugin for Certbot, the challenge is verified via a DNS TXT record, bypassing the HTTP proxy entirely:

sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d example.com \
  -d "*.example.com"

DNS-01 also lets you issue wildcard certificates, which HTTP-01 can't do.

3. The Web Root Doesn't Match

Certbot's webroot plugin needs to know where to place the challenge file. If the path in Certbot's renewal config doesn't match the directory nginx actually serves, the token gets written to the wrong place and validation fails.

Check what Certbot thinks the webroot is:

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

Look for the webroot_path line under [renewalparams]. Then compare it to what nginx is actually serving for that domain:

sudo nginx -T 2>/dev/null | grep -A5 "server_name example.com" | grep root

If they don't match — and they often don't after a server migration, a control panel reinstall, or switching from Apache to nginx — update the Certbot renewal config to point to the correct path.

On WordPress servers I manage, the webroot is typically /home/username/public_html or /var/www/example.com/public. But I've seen Certbot configs still pointing to /var/www/html from the initial setup, even after the site was moved.

4. The Certbot Timer Isn't Running

Certbot sets up a systemd timer (or cron job on older systems) that runs twice daily to check for certificates due for renewal. But I've seen it get disabled by OS upgrades, control panel reinstalls, or manual Certbot reinstallation.

Check if the timer is active:

sudo systemctl list-timers | grep certbot

If nothing shows up, check if the timer exists but is disabled:

sudo systemctl status certbot.timer

If the timer is missing entirely, check for the legacy cron approach:

sudo cat /etc/cron.d/certbot 2>/dev/null
sudo crontab -l 2>/dev/null | grep certbot

If none of these exist, nothing is triggering renewals. On systems using systemd, re-enable the timer:

sudo systemctl enable --now certbot.timer

If that unit doesn't exist (common on servers where Certbot was installed via pip or snap and then reinstalled differently), create a cron job:

echo "0 3,15 * * * root certbot renew --quiet --deploy-hook 'systemctl reload nginx'" | sudo tee /etc/cron.d/certbot-renew

I run certbot renew --dry-run on every server after setting this up to confirm the full pipeline works before a real renewal is needed.

5. Post-Renewal Hooks Aren't Reloading Nginx

This one is subtle. Certbot renews the certificate files on disk, but nginx keeps the old certificate loaded in memory. Without a reload, nginx continues serving the expired certificate even though the new one is sitting right there in /etc/letsencrypt/live/.

Check if a deploy hook is configured:

grep -r "renew_hook\|post_hook\|deploy_hook" /etc/letsencrypt/renewal/

If there's no hook, nginx won't reload after renewal. I set up a global deploy hook in /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh:

#!/bin/bash
nginx -t && systemctl reload nginx

Make it executable:

sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

The nginx -t before the reload is important — if another config change has broken nginx syntax, a blind systemctl reload would fail silently and you'd have a worse problem than an expired certificate.

For servers using Apache or LiteSpeed, substitute the appropriate reload command. For LiteSpeed, use /usr/local/lsws/bin/lswsctrl restart or systemctl restart lsws — the web server process holds the TLS certificates, not lsphp.

6. DNS Changed but Certbot Didn't Follow

When a domain's DNS records point somewhere other than your server — whether from a migration, a CDN change, or a misconfigured A record — HTTP-01 challenges fail because Let's Encrypt sends the validation request to wherever the DNS points, not to your server.

I see this most often after migrations. The site moves to a new server, DNS gets updated, but the old server's Certbot is still trying to renew certificates for domains it no longer serves. Meanwhile the new server was set up with a fresh certificate but nobody configured auto-renewal.

Check where DNS actually resolves:

dig +short A example.com
dig +short AAAA example.com

Compare those IPs to your server's actual IPs:

curl -s ifconfig.me
ip -6 addr show scope global | grep inet6

If they don't match, the certificate can't renew via HTTP-01 on this server. Either update DNS or switch to DNS-01 validation.

One less obvious variant: Let's Encrypt prefers IPv6 when AAAA records exist. If your server has an AAAA record pointing to an old IPv6 address, or if nginx isn't listening on IPv6, the challenge fails even though the IPv4 A record is correct. I've wasted hours on this one.

Diagnosing Failures After the Fact

When I inherit a server with an expired certificate, my first move is to check what went wrong:

sudo certbot renew --dry-run

This runs a simulated renewal for all certificates and reports any errors without actually making changes. The output tells me exactly which domain failed and why.

For more detail:

sudo cat /var/log/letsencrypt/letsencrypt.log | tail -100

And to see the current state of all certificates on the server:

sudo certbot certificates

This lists every managed certificate, its domains, expiry date, and file paths. For standard 90-day certificates, Certbot attempts renewal when less than 30 days remain. Certbot 4.x with ARI support uses a lifetime-based calculation instead, so the renewal window varies with certificate duration — especially relevant if you've opted into the shorter 45-day certificates. Either way, if a certificate is close to expiry and hasn't renewed, one of the six issues above is the cause.

The 45-Day Deadline Is Coming

All of this matters more now than it did six months ago. Let's Encrypt began offering opt-in 45-day certificates on 13 May 2026, and by February 2028 all certificates will have a 45-day lifetime instead of the current 90 days. That cuts the margin for renewal failures in half.

With 90-day certificates, Certbot's 30-day renewal window gives you 60 days of buffer. A broken renewal can sit unnoticed for weeks before the certificate actually expires. With 45-day certificates, that buffer shrinks to 15 days. A missed renewal cycle means the certificate expires within two weeks.

Certbot 4.1+ supports ACME Renewal Information (ARI), which lets Let's Encrypt tell your client exactly when to renew. Check your Certbot version:

certbot --version

If you're running anything below 4.1, upgrade. On Ubuntu/Debian, remove any OS-packaged version first, then install via snap:

sudo apt remove certbot -y
sudo snap install --classic certbot
sudo ln -sf /snap/bin/certbot /usr/bin/certbot

Without removing the old apt package, the system may still resolve certbot to the outdated binary. The snap install is the path I use on every server now — it auto-updates and stays current.

My Monitoring Setup

After being burned by silent renewal failures too many times, I now monitor certificate expiry across every server I manage. A simple cron job checks daily and alerts if any certificate is within 14 days of expiry:

#!/bin/bash
for cert in /etc/letsencrypt/live/*/cert.pem; do
  domain=$(basename "$(dirname "$cert")")
  expiry=$(openssl x509 -enddate -noout -in "$cert" | cut -d= -f2)
  expiry_epoch=$(date -d "$expiry" +%s)
  now_epoch=$(date +%s)
  days_left=$(( (expiry_epoch - now_epoch) / 86400 ))

  if [ "$days_left" -lt 14 ]; then
    echo "WARNING: $domain expires in $days_left days"
  fi
done

I pipe this through Healthchecks.io so I get a notification if it either reports a problem or fails to run at all. On top of that, Uptime Kuma can monitor certificate expiry directly via its HTTPS check — it alerts when a certificate is within a configurable number of days of expiry.

Stop Firefighting. Start Maintaining.

I manage 70+ WordPress sites for agencies and businesses. Whether you need ongoing maintenance, emergency support, or a one-off performance fix — I can help.

SSL renewal failures are exactly the kind of silent infrastructure problem that server management and maintenance plans catch before your customers do.

View Maintenance Plans | Get in Touch

Stop Firefighting. Start Maintaining.

I manage 70+ WordPress sites for agencies and businesses. Whether you need ongoing maintenance, emergency support, or a one-off performance fix — I can help.

View Maintenance Plans Get in Touch

Get in Touch to Discuss Your Needs