Spring Framework
@Async
Task Cancellation
Asynchronous Programming
Java

Spring Cancel Async Task

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Spring's @Async makes it easy to run work on a background executor, but cancellation is only possible if you keep a handle to the running task and the task cooperates. In other words, cancelling a Spring async task is not just a framework switch; it depends on the return type, the executor, and whether the code responds to interruption.

Return a Future or CompletableFuture

If an @Async method returns void, the caller has no direct cancellation handle. To cancel work, return Future or CompletableFuture.

java
1package example.async;
2
3import java.util.concurrent.CompletableFuture;
4
5import org.springframework.scheduling.annotation.Async;
6import org.springframework.stereotype.Service;
7
8@Service
9public class ReportService {
10
11    @Async("taskExecutor")
12    public CompletableFuture<String> buildReport() {
13        try {
14            for (int i = 0; i < 10; i++) {
15                if (Thread.currentThread().isInterrupted()) {
16                    throw new InterruptedException("Task was cancelled");
17                }
18
19                Thread.sleep(1000);
20            }
21
22            return CompletableFuture.completedFuture("done");
23        } catch (InterruptedException ex) {
24            Thread.currentThread().interrupt();
25            return CompletableFuture.failedFuture(ex);
26        }
27    }
28}

Now the caller can keep the returned future and request cancellation.

Cancelling from the Caller

Once you have the future, call cancel(true).

java
1CompletableFuture<String> future = reportService.buildReport();
2
3Thread.sleep(2500);
4boolean cancelled = future.cancel(true);
5
6System.out.println("cancelled = " + cancelled);

The true flag means interruption is requested for the executing thread when possible. But that does not guarantee the task stops immediately. The task code must either block in an interruptible call such as Thread.sleep() or explicitly check interruption state.

Configure an Executor You Control

Spring @Async uses an executor. Define one explicitly so task behavior is predictable.

java
1package example.async;
2
3import org.springframework.context.annotation.Bean;
4import org.springframework.context.annotation.Configuration;
5import org.springframework.scheduling.annotation.EnableAsync;
6import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
7
8import java.util.concurrent.Executor;
9
10@Configuration
11@EnableAsync
12public class AsyncConfig {
13
14    @Bean(name = "taskExecutor")
15    public Executor taskExecutor() {
16        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
17        executor.setCorePoolSize(4);
18        executor.setMaxPoolSize(4);
19        executor.setQueueCapacity(20);
20        executor.setThreadNamePrefix("async-");
21        executor.initialize();
22        return executor;
23    }
24}

With a dedicated executor, you can reason more clearly about queueing, shutdown, and interruption behavior.

Cancellation Is Cooperative

This is the most important concept. Calling cancel(true) does not forcibly kill arbitrary Java code. If the task is CPU-bound and never checks interruption, it may continue running.

For long loops, check interruption explicitly:

java
1for (int i = 0; i < items.size(); i++) {
2    if (Thread.currentThread().isInterrupted()) {
3        break;
4    }
5
6    process(items.get(i));
7}

For blocking I/O or external calls, cancellation may depend on the underlying client library. Some operations are interruptible, some are not.

Tracking Many Async Jobs

If users can start and cancel several tasks, keep a registry keyed by job ID.

java
1private final Map<String, CompletableFuture<?>> jobs = new ConcurrentHashMap<>();
2
3public String startJob() {
4    String jobId = UUID.randomUUID().toString();
5    CompletableFuture<String> future = reportService.buildReport();
6    jobs.put(jobId, future);
7    return jobId;
8}
9
10public boolean cancelJob(String jobId) {
11    CompletableFuture<?> future = jobs.get(jobId);
12    return future != null && future.cancel(true);
13}

This pattern is useful for REST APIs where one request starts work and another request cancels it later.

Common Pitfalls

The biggest pitfall is trying to cancel an @Async method that returns void. Without a future handle, you have no standard way to request cancellation for that invocation.

Another mistake is assuming cancel(true) forcefully stops all work. It only signals interruption. If the code ignores interruption, the task may continue.

Developers also sometimes use CompletableFuture.completedFuture(...) incorrectly for genuinely asynchronous work. If the heavy work has already happened before the future is returned, cancellation will not help.

Finally, remember that cancellation and cleanup belong together. If a task owns files, connections, or partial database state, interruption handling should leave the system consistent.

Summary

  • To cancel Spring @Async work, return Future or CompletableFuture, not void.
  • Keep the future and call cancel(true) from the caller.
  • Cancellation is cooperative, so task code must react to interruption.
  • Use a dedicated executor for predictable async behavior.
  • For user-cancellable jobs, store futures in a registry keyed by task or job ID.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.