One morning, a single one of my nightly jobs across several sites had stopped with a lone 401 in the log. The cause was mundane: months earlier I had tidied up a workspace, deleted an old key, and forgotten to swap it into one of the jobs. A key you delete by hand is hard to trace back later — which job used it, and when.
The Console now lets you set expiration dates on both API keys and Admin API keys. You can choose a preset, a custom date, or no expiry at all, and keys set to live longer than seven days get an email warning before they lapse. For an indie developer who leaves work to a model unattended, this quietly changes the relationship: instead of "a key might expire someday without warning," you get "this key expires on a date I know." The catch is that an expiring key, if you design around it carelessly, will stop your nightly job exactly on schedule. This article covers how to treat expiry as a plan rather than an accident.
Expiration turns "someday it lapses" into "it lapses on this date"
Until now, an API key lived until you or an admin explicitly deleted it. That sounds safe, but in practice it left the questions of "which key is old?" and "is a disused key sitting around unnoticed?" to human memory. An abandoned old key widens the damage if it ever leaks.
An expiration date inverts that relationship. The key lapses on the date you chose, and if you set it more than seven days out, you get an email as the deadline approaches. So instead of "it was already dead by the time I noticed," you get "I can see, seven days ahead, that it is about to lapse."
But there is a reality to unattended operation: relying on the notification alone is risky. Miss the email, and the job fails with a 401 the day the key expires. Handling expiry safely takes both a way to receive the warning and a way to swap the key before it lapses.
The fragile pattern is the "single key, one-shot swap"
The most breakable setup is holding a single production key, then, when the deadline arrives, creating a new one and deleting the old. That approach has a seam. In the few minutes between deleting the old key and reflecting the new one into your environment variables — or in any window where you forget to reflect it — the job fails to authenticate.
For unattended nightly runs, those few minutes of seam translate directly into "the whole night's work falls over." And the failure is silent; you do not notice until you read the log the next morning. Diagnosing the auth error itself is covered in How to Fix Claude API 401 Invalid API Key Authentication Error, but with expiration the focus shifts to not creating the seam in the first place.
Overlapping dual keys remove the seam
The basic move for removing the seam is to always hold two valid keys — one in active service, and one prepared as the next generation.
The steps go like this. When the active key (key-A) reaches seven days before expiry and the notification arrives, issue a new key (key-B) and give it an expiration date as well. Point your environment variable or secret store at key-B, run the nightly job once, and confirm it returns 200. Up to here key-A is still alive, so if the switch misbehaves you can fall back to it. Finally, watch key-B run for a day or two before revoking key-A.
This "overlap first, remove second" order is the crux. Delete the old key first and the whole point of dual keys evaporates. The scoped-rotation design itself is discussed in Stop the Bill Before It Balloons: Designing API Key Blast Radius for Unattended Pipelines. It helps to think of expiration as the mechanism that gives that rotation a deadline — an answer to "when do I do it?"
Don't wait for the Console email — let the job watch its own expiry
The notification email is useful, but a human receives it. Assuming the human will sometimes miss it, it pays to have the nightly job itself check how many days its key has left.
The approach is plain: when you issue a key, write its label and expiry date into a local ledger, and at the start of each nightly run, compute the days remaining. The Console API will not hand back a key's secret value later, but you can record the expiry date yourself.
import json
from datetime import date, datetime
from pathlib import Path
LEDGER = Path.home() / ".claude_key_ledger.json"
WARN_DAYS = 10 # margin so you notice before the Console's 7-day email
def check_key_expiry(active_label: str) -> None:
"""Call at the start of the nightly job. Warn when days remaining drop below the threshold."""
if not LEDGER.exists():
print("[key-check] No ledger found. Record expires_at when you issue a key.")
return
ledger = json.loads(LEDGER.read_text())
entry = ledger.get(active_label)
if not entry:
print(f"[key-check] Label {active_label} is not registered in the ledger.")
return
expires = datetime.strptime(entry["expires_at"], "%Y-%m-%d").date()
remaining = (expires - date.today()).days
if remaining < 0:
raise SystemExit(f"[key-check] {active_label} lapsed {abs(remaining)} days ago. Stopping.")
if remaining <= WARN_DAYS:
print(f"[key-check] Warning: {active_label} lapses in {remaining} days. Prepare the next key.")
else:
print(f"[key-check] {active_label} has {remaining} days left. All clear.")
# Example ledger (append when you issue a key)
# {
# "prod-nightly-2026-07": {"expires_at": "2026-10-10", "note": "nightly across four sites"}
# }
if __name__ == "__main__":
check_key_expiry("prod-nightly-2026-07")Put this as the first step of the nightly job, and even if you miss the Console email, the log records the days remaining every night. The threshold (WARN_DAYS) sits ahead of the Console's seven-day notice so you keep a margin to act on your own side. If the key has already lapsed, the job stops rather than blindly generating a pile of 401s.
Which expiry length to choose — a per-use rule of thumb
Set the expiry length by the key's purpose and the effort of swapping it. Shorter shrinks the survival window if it leaks, but raises how often you overlap keys. Here is the rule of thumb I use.
| Purpose | Suggested expiry | Reasoning |
|---|---|---|
| Production nightly batch | 60-90 days | A quarterly overlap is manageable. Watch it twice, via the email and the ledger. |
| Testing / experiments | 7-30 days | These tend to get abandoned. Keep them short so they clean themselves up. |
| CI / temporary automation | Avoid "no expiry" if you can | Consider dropping the key entirely and moving to keyless auth. |
| Local chat / drafting | 30-60 days | Even for personal use, build the habit of rotating on a schedule. |
For CI and temporary automation, it is worth considering not holding a long-lived static key at all. The idea of leaning on short-lived credentials issued per request is covered in Drop Your Static Claude API Keys: Moving CI and Production to Keyless Auth with Workload Identity Federation. Expiration is the safety valve when you keep using static keys; keyless auth is the move that reduces the number of keys altogether — different roles.
Wrapping up — your next step
Expiration turns a key from "a seed of worry that might lapse someday" into "a managed thing with a known expiry date." But since a human receives the notification, unattended operation benefits from pairing the overlapping dual-key swap with a local expiry ledger, so your nightly jobs stay up.
As a first step, set an expiry on the key you use in production today, write that date into a ledger, and add the script above to the front of your nightly job. Next time you rotate, you can practice the overlap-before-remove order. It is a small piece of groundwork, but it cuts down on that quiet failure where a lone 401 is all that is left in the morning log.