Skip to content
Spring Boot sb production 4 min read

Distributed Locks (ShedLock)

A @Scheduled method runs on every instance of your application. On a single node that’s exactly what you want — but the moment you scale to a cluster, a nightly billing job, a cleanup task, or a report generator fires N times in parallel, once per instance. The fix is a distributed lock: a shared mutex that lets only one instance execute the task while the others skip it. The simplest, most popular solution for scheduled jobs is ShedLock.

The problem

@Scheduled(cron = "0 0 2 * * *")   // 2 AM daily
public void sendInvoices() {
    invoiceService.generateAndEmailAll();
}

Deploy this on three instances and at 2 AM all three run sendInvoices(). Customers get three invoices, three emails, triple the database load. ShedLock ensures that across the whole cluster the task runs at most once per scheduled fire.

Note: ShedLock is a lock, not a scheduler. It does not guarantee a job runs somewhere if every node is down at the trigger time — it only guarantees that at most one node runs it. Pair it with the normal Scheduling support.

How it works

ShedLock keeps a single row (or key) in a shared store keyed by the lock name. Before a locked method runs, the instance tries to acquire the lock by writing that row with a locked_until timestamp. If another instance already holds an unexpired lock, the attempt fails and the method is skipped. The lock auto-expires after lockAtMostFor, so a crashed instance can’t deadlock the job forever.

Adding ShedLock

ShedLock needs the core module plus a LockProvider for your shared store. Here we use JDBC (any database you already run works).

<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-spring</artifactId>
    <version>6.3.0</version>
</dependency>
<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-provider-jdbc-template</artifactId>
    <version>6.3.0</version>
</dependency>

Create the lock table (this is the default schema; a Flyway migration is ideal):

CREATE TABLE shedlock (
    name       VARCHAR(64)  NOT NULL,
    lock_until TIMESTAMP    NOT NULL,
    locked_at  TIMESTAMP    NOT NULL,
    locked_by  VARCHAR(255) NOT NULL,
    PRIMARY KEY (name)
);

Configuration

Enable scheduling and schedule-locking, and declare the LockProvider bean.

import javax.sql.DataSource;
import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider;
import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
import org.springframework.context.annotation.*;
import org.springframework.scheduling.annotation.EnableScheduling;

@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "10m")
public class SchedulerConfig {

    @Bean
    public LockProvider lockProvider(DataSource dataSource) {
        return new JdbcTemplateLockProvider(
            JdbcTemplateLockProvider.Configuration.builder()
                .withJdbcTemplate(new org.springframework.jdbc.core.JdbcTemplate(dataSource))
                .usingDbTime()        // use the DB clock, avoids node clock skew
                .build());
    }
}

Locking a scheduled method

Add @SchedulerLock next to @Scheduled. The name must be unique per task.

import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class InvoiceJob {

    @Scheduled(cron = "0 0 2 * * *")
    @SchedulerLock(name = "sendInvoices",
                   lockAtLeastFor = "1m",
                   lockAtMostFor = "9m")
    public void sendInvoices() {
        // runs on exactly ONE instance per fire
        invoiceService.generateAndEmailAll();
    }
}

Output (cluster of three nodes at 2 AM):

node-a  acquired lock 'sendInvoices' (lock_until=02:09)  -> running job
node-b  could not acquire 'sendInvoices' -> skipped
node-c  could not acquire 'sendInvoices' -> skipped

The two timeouts

AttributeMeaningWhy it matters
lockAtMostForMaximum lock hold timeReleases the lock if the holder crashes mid-job; set comfortably longer than the worst-case runtime
lockAtLeastForMinimum lock hold timePrevents a fast job from running twice when node clocks differ slightly

Warning: If a job ever runs longer than lockAtMostFor, the lock expires and another instance can start a second copy. Always set lockAtMostFor larger than the longest plausible execution, but not so large that a crashed node blocks the job for hours.

Other lock providers

ShedLock supports many backends — use whichever shared store you already operate:

<!-- Redis instead of JDBC -->
<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-provider-redis-spring</artifactId>
    <version>6.3.0</version>
</dependency>

JDBC, Redis, and MongoDB providers all expose the same LockProvider interface, so swapping is a one-bean change.

Alternatives

ApproachGranularityBest for
ShedLockPer scheduled taskThe common case: cron jobs in a cluster
Leader election (e.g. Spring Integration LeaderInitiator, k8s lease)One elected leader runs all leader workWhen one node should own a whole set of responsibilities
Redisson RLockGeneral distributed lockLocking arbitrary business operations, not just schedules

Leader election designates a single instance as leader and runs all scheduled work there; if it dies, another is elected. Redisson offers full-featured distributed locks (fair locks, read/write locks) for locking any operation, not just @Scheduled methods.

Best Practices

  • Add @SchedulerLock to every @Scheduled task that must run once per cluster.
  • Give each lock a unique, stable name; reusing a name lets unrelated jobs block each other.
  • Set lockAtMostFor safely above the worst-case runtime to survive crashes without deadlock.
  • Use usingDbTime() (or a single source of time) to avoid clock-skew bugs across nodes.
  • Reuse a store you already run (your database or Redis) rather than adding new infrastructure.
Last updated June 13, 2026
Was this helpful?