# ShedLock: Preventing Duplicate Execution of Spring Scheduled Tasks

> ShedLock is a Java library that ensures a scheduled task runs on at most one node at a time in a distributed Spring application. It stores and checks time-based locks in an external store such as a SQL database, Redis, or MongoDB, but it is not a scheduler and will not delay or queue tasks on other nodes.

**lukas-krecan/ShedLock** — Distributed lock for your scheduled tasks

- Repository: https://github.com/lukas-krecan/ShedLock
- Stars: 4,224 · Forks: 571
- Language: Java
- License: Apache-2.0
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/lukas-krecan-shedlock

## The Problem ShedLock Addresses

When a Spring application runs on multiple nodes, the built-in @Scheduled mechanism runs the same task on every node simultaneously. For tasks that modify shared state, send emails, or call external APIs, this produces duplicate work and inconsistent results. ShedLock prevents that by acquiring a named lock before each task execution and releasing it on completion.

The README is explicit about what ShedLock does not do: it will never be a full scheduler. Tasks on other nodes are not delayed or queued when the lock is held; they are simply skipped. This makes ShedLock appropriate for tasks that are safe to miss but not safe to run twice, such as sending a digest email or compacting a log table. If a task must execute on exactly one node and must not be skipped under any circumstance, a dedicated distributed scheduler such as db-scheduler or JobRunr is the stated alternative.

## How the Time-Based Lock Works

ShedLock uses an external store to record three fields per task: the task name as a primary key, the locked_at timestamp when the lock was acquired, and the lock_until timestamp when the lock expires. A node acquiring a lock writes these fields. Before running, it checks whether an unexpired entry with the same name already exists.

Two timing parameters control behavior. The lockAtMostFor attribute is a safety net: it sets the maximum duration a lock can be held, so a lock is always released even if the node crashes before the task finishes. The lockAtMostFor value must be set to a duration significantly longer than the expected task runtime, since exceeding it causes more than one node to hold the lock and the resulting behavior is unpredictable, as the README states. The lockAtLeastFor attribute prevents re-execution of short tasks on different nodes caused by small clock differences; the lock remains held for at least this duration even if the task completes faster.

ShedLock requires that clocks on all nodes are synchronized. The library makes no attempt to compensate for clock drift between nodes. The README states this as a precondition, not a limitation that might be removed.

## Adding ShedLock to a Spring Project

Integration starts with the Maven dependency for the Spring integration module:

```xml
<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-spring</artifactId>
    <version>7.10.1</version>
</dependency>
```

Enable scheduled locking on the Spring configuration class with the @EnableSchedulerLock annotation, setting a default lock duration:

```java
@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "10m")
class MySpringConfiguration {
}
```

Annotate each task method with @SchedulerLock, giving it a unique name. Calling LockAssert.assertLocked() inside the method is optional but recommended: it throws an exception at startup if the lock proxy is not properly configured, surfacing misconfiguration early rather than during a production incident:

```java
@Scheduled(...)
@SchedulerLock(name = "scheduledTaskName")
public void scheduledTask() {
    LockAssert.assertLocked();
    // do something
}
```

For a 15-minute recurring task that should run at most once per interval regardless of how many nodes are up, the recommended configuration is:

```java
@Scheduled(cron = "0 */15 * * * *")
@SchedulerLock(name = "scheduledTaskName", lockAtMostFor = "14m", lockAtLeastFor = "14m")
public void scheduledTask() {
    // do something
}
```

Setting lockAtLeastFor to 14 minutes means that even if the task completes in under a minute, no other node can run it again until the 14-minute hold expires.

## Lock Provider Setup: SQL Table Structure

The lock provider stores lock state in the chosen backend. The JDBC template provider, which covers MySQL, PostgreSQL, MariaDB, Oracle, and others, requires a dedicated shedlock table. The name column must be the primary key:

```sql
CREATE TABLE shedlock(name VARCHAR(64) NOT NULL, lock_until TIMESTAMP(3) NOT NULL,
    locked_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), locked_by VARCHAR(255) NOT NULL, PRIMARY KEY (name));
```

ShedLock supports over 20 additional backends including Redis, MongoDB, DynamoDB, ZooKeeper, Hazelcast, Elasticsearch, Cassandra, Etcd, and cloud-native options such as CosmosDB, Firestore, GCS, and S3. Each backend has its own Maven artifact under the net.javacrumbs.shedlock group. For example, the Redis provider using Spring RedisConnectionFactory has its own shedlock-provider-redis-spring artifact.

The repository also includes an In-Memory provider for use in tests, where a real external store is not available. Using the In-Memory provider in production would defeat the purpose of distributed locking, since each node has its own isolated in-memory state.

## Where ShedLock Falls Short

ShedLock releases a lock when lockAtMostFor expires, regardless of whether the task is still running. If a task takes longer than lockAtMostFor, a second node may acquire the lock and start the same task while the first is still executing. The README calls the resulting behavior unpredictable, and it places the responsibility on the developer to set lockAtMostFor to a value significantly longer than the worst-case task runtime.

ShedLock does not support dynamic task registration. Every lock must have a unique name defined at annotation time. Scenarios that require creating tasks programmatically at runtime, or that need retry logic, backoff, or guaranteed delivery, are outside its scope.

The library also assumes clock synchronization across nodes. On cloud deployments where NTP synchronization is inconsistent, locks can expire prematurely or overlap, leading to double execution. The README documents this as a hard precondition rather than a configuration option.

## Quartz: Full Scheduling at a Different Trade-Off

Quartz is a mature Java scheduling library that stores job definitions, triggers, and execution history in a database. Unlike ShedLock, it manages the entire lifecycle of a job: when it fires, which node runs it, whether it retries on failure, and how execution history is retained. Quartz provides true distributed scheduling with cluster support, meaning only one node executes a given trigger at a time by design.

The trade-off is complexity. Quartz requires schema migrations for its own tables, explicit job and trigger definitions in code or XML, and a database connection pool configuration. ShedLock works as a thin annotation layer on top of Spring's existing @Scheduled mechanism, adding only the lock table without changing how jobs are declared. Applications that already use @Scheduled and need only a simple guarantee against parallel execution pay a much smaller integration cost with ShedLock than with a full migration to Quartz.

## Maintenance and License

The last push to the repository was on 2026-09-25. The repository is not archived. ShedLock is licensed under the Apache License 2.0, which permits use in commercial products without copyleft requirements. The repository includes a compatibility matrix in the README documenting which versions of ShedLock work with which Spring Boot versions, which is relevant before upgrading either dependency.

## Conclusion

ShedLock fits Spring or Micronaut applications where scheduled tasks are safe to skip but not safe to run in parallel on multiple nodes. It is the wrong tool when you need true queuing or guaranteed execution across all nodes. Before adopting it, confirm that your lock provider is accessible from all application nodes and that the clocks on those nodes are synchronized, since ShedLock's time-based release logic depends on clock agreement.

## FAQ

### What does ShedLock do?

ShedLock prevents a scheduled task from running simultaneously on multiple nodes of a distributed application. It acquires a named lock in an external store before executing a task and releases it when the task finishes, so other nodes see the lock and skip their execution of the same task.

### What is the ShedLock table used for?

The shedlock table in the database stores one row per named task, recording the task name as a primary key, when the lock was acquired (locked_at), and when it expires (lock_until). All nodes check and update this table to coordinate which node holds the current execution rights.

### What is ShedLock in Spring Boot?

In Spring Boot, ShedLock is a library that adds distributed locking on top of @Scheduled methods. You enable it with @EnableSchedulerLock on a configuration class and annotate each task method with @SchedulerLock, specifying a lock name and a maximum hold duration.

### How do you use ShedLock?

Add the shedlock-spring dependency, annotate your Spring configuration with @EnableSchedulerLock, add @SchedulerLock with a unique name and lockAtMostFor value to each scheduled method, and configure a lock provider such as the JDBC template provider backed by a shedlock database table.

## Sources

- [Issues](https://github.com/lukas-krecan/ShedLock/issues)
- [License: Apache-2.0](https://github.com/lukas-krecan/ShedLock/blob/master/LICENSE)
- [lukas-krecan/ShedLock on GitHub](https://github.com/lukas-krecan/ShedLock)
- [README](https://github.com/lukas-krecan/ShedLock/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/lukas-krecan-shedlock
