Durable jobs, leases, retries, recurrence, dead-lettering and monitoring
coreadvancedA job scheduler is a durable list of work with a claiming protocol on top, and almost every design question resolves to "what happens if a worker dies holding this job". Jobs are stored durably before anyone tries to run them, with a run-at time, so scheduling is a query — pick up everything due — rather than an in-memory timer that a restart forgets. Workers claim jobs with a lease rather than a lock: a lease is a claim with an expiry, so a worker that crashes stops renewing and the job becomes claimable again automatically, with no operator involved and no lock left dangling forever. That single mechanism is why leases are preferred to locks for work distribution, and it is also why jobs must be idempotent: a lease can expire while the original worker is merely slow, so the job can genuinely run twice. Retries have a backoff and a maximum attempt count, and when that count is exhausted the job goes to a dead-letter queue rather than being retried forever or dropped — a dead letter is a job you can inspect, fix and replay, which a discarded one is not. Recurrence is expressed as a schedule that produces a new job for each occurrence, keyed by occurrence so a scheduler running on two machines cannot produce two runs of the same nightly report. Deduplication is that same key applied to enqueue: an at-least-once producer can submit the same job twice, and a unique key on the occurrence makes the second submission a no-op. And monitoring watches queue depth, oldest-job age and dead-letter rate, because a scheduler fails quietly — nothing is broken, work just stops happening.
Think of it as
A pile of job cards and a set of pigeonholes. A worker takes a card and pins their name and a time to it: "mine until 10:15". If they finish, the card is filed as done. If they are still working at 10:14, they re-pin a later time. If they fall over, nobody has to notice — at 10:15 the card is unpinned automatically and someone else takes it. Every hard question about job systems becomes easy in this picture: a slow worker and a dead worker look identical from outside, so the card can be worked twice, so the work has to be safe to repeat.
What we're doing: Watch a worker die mid-job, then watch a slow worker lose its lease, and see why both end at the same requirement.
- 9
- This is the entire argument for leases over locks. A lock held by a terminated process needs something to detect the death and release it; an expiry needs nothing, so recovery is a property of the mechanism rather than of an operator being awake.
- 18
- The same property that recovers case A creates case B. You cannot have automatic recovery from crashes without accepting that "crashed" and "slow" look identical, which is precisely why at-least-once is the guarantee on offer.
- 29
- The remedy lives in the handler because it is the only place that knows what the work means. A unique constraint on (account, period) makes the second execution a no-op regardless of how many times the job runs.
Why this works: Lengthening the lease does not solve case B, it only makes it rarer while making case A slower to recover — the two are the same dial pulled in opposite directions. The design accepts at-least-once execution and puts correctness in the handler, which is the only place with enough context to define what "already done" means.
Setting a lease shorter than the job it covers
Wrong
Better
What you see: Jobs are executed several times each under normal conditions, worker CPU is spent on duplicate work, and the queue drains far more slowly than the worker count suggests it should — while every individual worker appears healthy.
Why: A fixed lease has to be longer than the slowest normal run, which for variable work means an uncomfortably long lease and correspondingly slow crash recovery. Heartbeat renewal decouples the two: the lease stays short, so a crash is detected quickly, while a healthy worker keeps extending its claim for as long as it genuinely needs.
- scheduled (run_at in the future) (start)
- → ready (due, unclaimed) when run_at reached
- ready (due, unclaimed)
- → leased (a worker holds it) when a worker claims it
- leased (a worker holds it)
- → done when handler succeeds
- → ready (due, unclaimed) when lease expires — worker crashed or stalled
- → ready (due, unclaimed) when handler fails, attempts remain — backoff
- → dead-lettered when max attempts exhausted
- done (end)
- dead-lettered (end)
- → ready (due, unclaimed) when operator replays after a fix
Lease versus lock for distributing work
Ten requirements, and where each one lives
Remember: Persist jobs with a `run_at` so scheduling is a query, and claim them with a lease — a claim that expires — so a crashed worker releases its job automatically. That same expiry means a slow worker can lose a job it is still running, so at-least-once execution is the guarantee and idempotent handlers are mandatory. Renew leases by heartbeat rather than setting them long, retry with backoff up to a cap, dead-letter on exhaustion so failures can be inspected and replayed, key recurrence and deduplication by occurrence, and alert on oldest-job age — because a scheduler that stops produces no errors at all.
See also: leader follower heartbeats and leases · prefer simpler mechanisms · max attempts and dead lettering · retries and dead lettering · idempotent consumer design · symptom based alerting

