Use case
Cooldowns that clean up after themselves
Set a key with a lifetime and its existence answers the question. When the window passes it disappears on its own, which is the whole trick.
The problem
An agent that can act repeatedly will, given the chance, act repeatedly. It retries a failing API three times a second. It alerts you about the same broken thing on every pass. It re-deploys because the last deploy has not finished yet.
What you want is a window: once this happens, do not let it happen again for an hour. Expressing that without somewhere to remember means asking the model to store a timestamp, work out the current time, subtract one from the other, and compare against a threshold. Every step there is a chance to be wrong, and the failure is silent.
The pattern
Write a key with a ttl in seconds. Nothing else. The key existing means
you are inside the window; the key being gone means you are not:
# Am I clear to act?
get_value("deploys:staging:cooldown")
→ { exists: false }
# Yes. Act, then hold the window for an hour.
set_value("deploys:staging:cooldown", "held", ttl: 3600)
→ { created: true, expires_at: "2026-08-15T10:00:00+00:00" }
Anything that runs in the next hour gets a different answer:
get_value("deploys:staging:cooldown")
→ { exists: true, value: "held", expires_at: "2026-08-15T10:00:00+00:00" }
No subtraction. No date parsing. The question "has an hour passed" becomes the question "does this key exist", which a model gets right every time.
Tell your agent
Before alerting me about a failing check, use Junkt to
get_value the key "alerts:health-check:cooldown".
If it exists, stay quiet — I have already been told
recently. Do not alert.
If it does not exist, send the alert, then set_value that
key to "sent" with a ttl of 3600.
Backoff
Growing the window after repeated failures needs the retry count as well, which is a counter. The two work together:
increment("jobs:sync:failures")
→ { value: 3 }
# back off for 2^3 minutes
set_value("jobs:sync:cooldown", "backing off", ttl: 480)
On success, delete the failure counter so the next problem starts from a short window again. This is one of the few places where you do want the model doing arithmetic, and it is simple enough to be safe — but pin the exponent table in the prompt rather than leaving the schedule to its judgement.
Cooldowns are not locks
Worth being straight about this. The check-then-set sequence above has a gap between the read and the write, so two runs starting at the same instant can both see an empty key and both proceed. For rate limiting, alerting and backoff — where the cost of an occasional double is low — that is fine, and this pattern is the right amount of machinery.
If a double would be genuinely expensive, do not use this. Use
increment instead, which is atomic, and treat a returned value of 1 as
"you have the lock":
increment("deploys:staging:lock")
→ { value: 1, created: true } # you got it
increment("deploys:staging:lock")
→ { value: 2 } # someone else is in there
Set a TTL on the key first so a crashed holder does not block the lock forever, and delete it when you are done.
TTL and your key budget
Expired keys stop counting toward your 25 the moment they elapse, before the hourly prune physically removes them. Short-lived state is effectively free, which is why 25 keys goes further than it sounds when most of them clean up after themselves.
Worth knowing
- Re-setting rewrites the TTL. Calling
set_valueagain on a live cooldown extends it from now rather than topping it up. - Setting without a ttl clears it. A later write that forgets the
ttlturns a self-cleaning key into a permanent one. - The minimum is one second. Anything shorter is rejected.
- Put the window in the key name if it helps —
alerts:health-check:cooldown-1h— so a glance atlist_keystells you what you set up months ago.
State that tidies itself away
Set a ttl and stop thinking about it. Free tier, 25 keys, no card — and expired keys do not count against them.