prev: [Clock skew](04-clock-skew.md) · [README](../README.md) # 5. When not to use a lock ShedLock is small, it has one dependency on your database, and it solves the stated problem. It is still worth asking whether the problem should exist. **Make the job idempotent instead.** A lock is a way of avoiding a second execution. Idempotence is a way of not caring about one. If the job's work can be keyed — "mark invoices dated 2026-09-01 as sent, where they are not already marked" — then three replicas running it produce one outcome and you have removed a distributed-systems dependency rather than adding one. This is almost always the better engineering, and it is almost never what the article you searched for suggests. **A lock is not a guarantee of exactly-once.** It is a guarantee of at-most-one-per-lock-window, which is different. If the holder dies halfway, the job did not complete and nothing retries it — ShedLock has no notion of failure, only of expiry. Pairing it with a job table that records completion is what makes "exactly once" true, and at that point the job table is doing most of the work. **Consider the scheduler you already have.** A Kubernetes `CronJob` runs one pod per schedule and needs no lock, no library and no table; it costs you a pod start per run and the schedule lives outside the application. Quartz in clustered mode owns misfire policy, persistence and recovery, at the price of eleven tables and a great deal of configuration. ShedLock sits between them: it adds a lock to the scheduler you already have, and deliberately adds nothing else. **A lock provider needs its store to be consistent.** The JDBC provider is safe because a conditional `UPDATE` on one row is atomic in every relational database. The Redis provider is safe on a single instance and, as its own documentation notes, is subject to the well-known argument about locks over Redis replication. If your store is eventually consistent, your lock is too. ## A short checklist for the version you ship - `@EnableSchedulerLock` present, and a `LockProvider` bean present — without both, the annotation does nothing ([chapter 1](01-three-replicas-three-executions.md)) - `usingDbTime()` on ([chapter 4](04-clock-skew.md)) - `lockAtMostFor` longer than the worst-case run time, and you have accepted the gap it implies after a crash - `lockAtLeastFor` non-zero for any job shorter than the interval between two replicas' timers - `spring.task.scheduling.pool.size` raised above 1 ([chapter 3](03-one-scheduler-thread.md)) - the lock names are unique per job — two jobs sharing a name share a lock, and one of them stops running - the method carrying `@SchedulerLock` is called from outside its own class, and is not `final` [README](../README.md)