Expiry and rotation

A client presenting an expired certificate is the most common silent mTLS failure, and certificates keep getting shorter-lived: cert-manager renews a mounted Secret at two-thirds of its lifetime, Vault PKI issues certificates measured in hours. A client snapshots its certificate at construction, so a long-running process needs a plan for what happens next.

httpx-pki gives you three, in increasing order of automation:

  • Warn early — know a rollover is coming

  • Reload — pick up the new file, manually or automatically

  • Fail loudly — turn an expired certificate into a clear error instead of a handshake failure

Warning before it expires

Loading an already-expired or not-yet-valid certificate warns immediately:

CertificateValidityWarning: client certificate expired on 2026-08-01;
mTLS handshakes will fail.

To hear about one that is merely about to roll over, pass warn_if_expires_within — accepted by every constructor, from_* included:

from datetime import timedelta
from httpx_pki import PKIClient

client = PKIClient(
    "client.p12",
    password="secret",
    warn_if_expires_within=timedelta(days=14),
)
CertificateValidityWarning: client certificate expires on 2026-08-07
(in 4 day(s)).

The window is kept on the client, so it keeps applying for the client’s lifetime: every reload re-checks the freshly loaded certificate against it, and it survives pickling. A rotation that lands another short-lived certificate warns again; one that lands a healthy certificate goes quiet. That is what makes it useful next to auto_reload — see Strict validity.

To check on demand rather than be warned, the validity properties and check_validity() are covered in Inspecting a certificate.

Reloading a rotated certificate

reload() re-reads the source — file, from_env variables, Windows store, or macOS keychain — and swaps the fresh certificate into the mounted SSL context in place, so new handshakes present it immediately:

client = PKIClient("/etc/certs/client.pem")

# ... cert-manager rotates /etc/certs/client.pem ...

client.reload()

Automatically

auto_reload stats the source files before each request and reloads when they change, throttled to at most once per second by default:

from datetime import timedelta

PKIClient("/etc/certs/client.pem", auto_reload=True)
PKIClient("/etc/certs/client.pem", auto_reload=timedelta(seconds=30))

Nothing else in your code changes — the next request after a rotation simply presents the new certificate.

What to expect

The swap is atomic. If the rotated file is unreadable or garbage, reload() raises CertificateLoadError and the previous certificate keeps serving:

CertificateLoadError: invalid PKCS#12 data or wrong password

With auto_reload that error surfaces on the triggering request and is retried on the next one. You never end up with a client that has no certificate.

Established connections keep their certificate. TLS has no mid-connection re-authentication, so only new connections present the rotated certificate. Existing ones carry on until they close.

Rotation tooling should replace files atomically — write-then-rename, which kubelet and cert-manager already do. A reload that catches a half-written file raises rather than mounting garbage, but atomic replacement avoids the churn.

Note

auto_reload needs a filesystem path to watch. Constructing from in-memory bytes, the Windows store, or the macOS keychain raises:

TypeError: auto_reload requires a filesystem-path certificate source to watch

The stores can still be re-exported with a manual reload() — see Reloading and Reloading.

Passwords and unattended reloads

Reloading a password-protected source needs the password again. httpx-pki does not retain it by default:

client = PKIClient("client.p12", password="secret")

client.reload()                      # CertificateLoadError
client.reload(password="secret")     # works

Enabling auto_reload does retain the password on the client, since an unattended reload has no other way to decrypt the source:

client = PKIClient("client.p12", password="secret", auto_reload=True)
client.reload()                      # works — password retained

That is a deliberate trade: it keeps the password in memory for the client’s lifetime. See Security notes.

Note

password= applies only to sources httpx-pki decrypts on your behalf — a PKCS#12 or PEM bundle, or a separate key file. Three sources supply their own, and passing one to them raises rather than being quietly discarded:

TypeError: reload(password=...) does not apply to a from_env() client: the
password is read from HTTPX_PKI_PASSWORD along with the rest of the
configuration. Set that variable instead of passing one here.

TypeError: reload(password=...) does not apply to a client built from the
Windows certificate store: the certificate is exported under an internally
generated single-use password, so there is none to supply. Drop the argument.

The macOS keychain says the same as the Windows store.

Strict validity

strict_validity=True runs check_validity() before every request, so a certificate that expired anyway fails clearly before the connection is attempted:

client = PKIClient("client.p12", password="secret", strict_validity=True)
client.get("https://mtls.example.com/")
CertificateExpiredError: client certificate expired on 2026-08-01 18:02 UTC

Without it you get an opaque OpenSSL handshake error from the far side instead — which is the failure this whole page exists to prevent.

Combining it with auto_reload is the belt-and-braces setup for a long-lived service: pick up rotations automatically, and fail legibly if one is ever missed.

PKIClient(
    "/etc/certs/client.pem",
    auto_reload=True,
    strict_validity=True,
    warn_if_expires_within=timedelta(days=7),
)

Silencing the validity warnings

The warnings on this page are all CertificateValidityWarning, so a single filter quiets them without touching anything else httpx-pki reports:

import warnings
from httpx_pki import CertificateValidityWarning

warnings.filterwarnings("ignore", category=CertificateValidityWarning)

See Exceptions and warnings for the other categories and for making warnings fatal.

Next steps