Avoid performing Spring Async task twice at the same time
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
@Async in Spring makes it easy to run work on a background executor, but it does not automatically prevent the same task from being started twice. If the method can be triggered by multiple requests, schedulers, or events, duplicate execution is still possible unless you add explicit coordination.
The right solution depends on scope. If you only care about one JVM instance, an in-memory lock may be enough. If the application runs on several nodes, you need a shared coordination mechanism such as a database lock or a distributed lock.
Why @Async Alone Does Not Prevent Duplicates
An async method just means "submit this work to an executor":
If two callers invoke generateNightlyReport() at nearly the same time, Spring will happily submit both tasks. There is no built-in singleton-task guarantee here.
Use an In-Memory Guard for a Single Instance
If only one application instance exists, a simple atomic flag can work:
This prevents overlap inside one JVM. It is simple and effective for one-node deployments, but it does nothing across multiple pods or servers.
Use a Lock That Matches Deployment Scope
In clustered deployments, you need coordination outside process memory. One common pattern is a lock row in a database or a distributed lock provider.
A conceptual database-lock flow looks like this:
The exact implementation varies, but the principle is stable: the lock must live in a system shared by every application instance that could schedule the task.
Scheduler Example with Guarding
This issue often appears with scheduled jobs:
If the job sometimes runs longer than the schedule interval, overlap becomes likely. That is exactly when a guard or lock is required.
Keep Failure Handling Safe
Whatever lock strategy you use, always release the lock in finally. Otherwise a thrown exception can leave the task permanently blocked.
You also need to decide what "already running" should mean operationally:
- skip the new request
- queue the new request
- return an explicit status to the caller
That policy is a business decision, not just a concurrency detail.
Common Pitfalls
- Assuming
@Asyncprevents duplicate execution by itself. - Using an in-memory flag in a multi-instance deployment.
- Forgetting to clear the running flag or release the lock on failure.
- Scheduling work more often than the task can realistically finish.
- Solving a business workflow problem with only thread-level thinking.
Summary
- '
@Asyncruns work in the background but does not prevent duplicate starts.' - For one JVM, an
AtomicBooleanor similar in-memory guard can work. - For multiple instances, use a shared lock such as a database or distributed lock.
- Always release the lock in a
finallyblock. - Decide whether duplicate triggers should be skipped, queued, or reported explicitly.

