---
title: "Renewals — Certificates for Laravel | Roundly"
description: "Renew certificates nearing expiry with Certificates::renewDue() or certificates:renew — inline or queued, every outcome reported, on your schedule."
url: https://roundly-consulting.com/open-source/docs/certificates-for-laravel/renewals
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Certificates for Laravel](https://roundly-consulting.com/open-source/docs/certificates-for-laravel.md)

# Renewals

A certificate is expiring once it is issued, renewed or failed and its live certificate expires within renewal.threshold\_days (default 21) — or already has. Certificates::renewDue() renews every such certificate — or queues each one — and returns a RenewalReport:

```php
use RoundlyConsulting\Certificates\Facades\Certificates;

$report = Certificates::renewDue();          // everything inside renewal.threshold_days → RenewalReport
Certificates::renewDue(7, queue: true);      // a 7-day window, one queued job per certificate

if ($report->hasFailures()) {
    foreach ($report->failed as $failure) {
        logger()->warning("Renewal failed for {$failure->certificate->domain}: {$failure->reason()}");
    }
}

$report->renewedDomains();   // ['a.example.com', …]; also queuedDomains(), failedDomains(), count(), isEmpty()
```

Each due certificate is attempted on its own: one failure never stops the rest. The report lists renewed, queued (with queue: true) and failed — each failure a RenewalFailure carrying the certificate and the exception, with reason() for its message. Every failure is also passed to your exception handler. A failed inline renewal leaves the row failed with CertificateFailed fired; a failed dispatch leaves the row untouched, so the next run retries it.

## One certificate

renew() and renewLater() take a registry Certificate or a domain (its most recent row), flat or through the for() handle. An unknown domain, or a status that cannot renew (a revoked certificate, say), throws CertificateException — renewLater() checks the status before it queues anything:

```php
use RoundlyConsulting\Certificates\Facades\Certificates;

Certificates::renew('app.example.com');        // now, through the certificate's own driver
Certificates::renewLater('app.example.com');   // queue RenewCertificateJob (renewal.queue)
Certificates::renew($certificate);             // a registry row works as well as a domain

Certificates::for('app.example.com')->renew();                // same verbs on the domain handle
Certificates::for('app.example.com')->using('acme')->renewLater(); // only the acme row is touched

Certificates::expiring(14);   // Collection<int, Certificate> expiring within 14 days, soonest first
```

## From the console

certificates:renew runs Certificates::renewDue() — or, given a domain, Certificates::renew() for its most recent row (renewLater() with --queue). It prints every certificate’s outcome and exits non-zero when any renewal or dispatch failed, so the scheduler’s failure hooks fire:

```bash
# everything expiring within renewal.threshold_days
php artisan certificates:renew

# one domain, whatever its expiry
php artisan certificates:renew shop.example.com

# a wider window, renewed through the queue
php artisan certificates:renew --threshold=30 --queue
```

## Scheduling

The package registers no schedule — keep that in your app, for example in routes/console.php:

```php
use Illuminate\Support\Facades\Schedule;
use RoundlyConsulting\Certificates\Facades\Certificates;

Schedule::command('certificates:renew')->daily()->emailOutputOnFailure('ops@example.com');

// or, without the command:
Schedule::call(fn () => Certificates::renewDue(queue: true))->daily();
```

emailOutputOnFailure() mails the command’s output whenever certificates:renew exits non-zero — when any inline renewal or any dispatch failed — so it needs a working mailer. With --queue, a renewal that fails later inside RenewCertificateJob doesn’t change the exit code: the row moves to failed and CertificateFailed fires, which raises an alert when alerts.enabled is on.

## The renewal lifecycle

- renewDue() dispatches CertificateExpiring for each certificate it picks up — daysUntilExpiry is negative once it has expired.
- Issued, renewed and failed certificates can renew, as can a stuck renewing row (below) — anything else throws CertificateException for an illegal status transition.
- The renewal holds the certificate’s provisioning lock — the one issue() takes — and moves the row to renewing only while it still has the status this process read. A concurrent renewal, a renewal racing issue() or a stale model throws ProvisioningInProgressException instead of renewing twice.
- The certificate’s own driver re-provisions every domain it covers — a SAN certificate keeps all its hosts.
- The renewal is proven by the provider’s own report: a status that is not live, the very same certificate (same fingerprint, e.g. imported material nobody replaced) or — for a driver that reports no fingerprint, such as kubernetes — an expiry that did not move later is a failed renewal.
- Otherwise the row becomes renewed with the reported expiry (90 days out when the driver reports none), issuer, serial and fingerprint, last\_renewed\_at is stamped and CertificateRenewed fires. The certificate’s cached status report is dropped.
- A failed renewal moves the row to failed, fires CertificateFailed and rethrows the exception — it never sits in renewing, so it raises an alert instead. Failed is not a dead end: expiring(), renewDue() and certificates:renew keep retrying it, before and after its live certificate runs out.
- A renewal cut short — a queue timeout during ACME polling, a deploy — leaves the row in renewing. Once it has been untouched for longer than lock.locked\_for\_seconds it counts as renewable again: expiring() and renewDue() pick it up, renew() and renewLater() accept it, and only one process takes it over.

## Per driver

acme places a fresh order with a new certificate key and overwrites the stored material. kubernetes finds the Ingress TLS entry already in place and leaves it untouched — cert-manager rotates the secret itself — then records the notAfter it reports; a notAfter that did not move later fails as not renewed, since cert-manager keeps reporting Ready while its own renewal fails. filesystem with self\_signed mints fresh material; without it, replace the PEM files before renewing, or the renewal fails as not renewed.

## Queued renewals

renewLater(), renewDue(queue: true) and certificates:renew --queue dispatch a RenewCertificateJob per certificate instead of renewing inline. The job lands on the queue named by renewal.queue (CERTIFICATES\_RENEW\_QUEUE), re-reads the row on the database connection it was queued from and skips certificates deleted in the meantime. A job renewDue(queue: true) queued also does nothing once the certificate is no longer due, so overlapping sweeps never re-renew a certificate that was just renewed; a renewLater() job always renews.

The job’s timeout is lock.locked\_for\_seconds (600 seconds by default), so a worker does not kill an ACME renewal mid-poll. Keep your queue connection’s retry\_after above it.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/certificates-for-laravel.md)
