Spring Boot
Async
Multithreading
Java
Troubleshooting

Spring Boot Async method not running in separate Thread

Interview Questions practice on Codemia

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

Browse interview questions

Introduction

A frequent Spring Boot issue is adding @Async to a method and finding it still runs on the caller thread. This usually happens because async proxying is not active, method invocation bypasses the proxy, or executor configuration is missing. Correct async behavior requires both annotation setup and call-path design.

Enable Async Infrastructure First

@Async works through Spring proxies, so the application must enable async support.

java
1import org.springframework.boot.SpringApplication;
2import org.springframework.boot.autoconfigure.SpringBootApplication;
3import org.springframework.scheduling.annotation.EnableAsync;
4
5@EnableAsync
6@SpringBootApplication
7public class App {
8    public static void main(String[] args) {
9        SpringApplication.run(App.class, args);
10    }
11}

Without @EnableAsync, methods annotated with @Async run normally and can appear to ignore async intent.

Use @Async in a Separate Spring Bean

Self-invocation is the most common mistake. If a class calls its own @Async method directly, proxy interception is bypassed.

java
1import org.springframework.scheduling.annotation.Async;
2import org.springframework.stereotype.Service;
3
4@Service
5public class NotificationService {
6
7    @Async
8    public void sendEmail(String email) {
9        System.out.println("Thread: " + Thread.currentThread().getName());
10        // expensive work
11    }
12}

Call this method from another Spring-managed bean, not from this.sendEmail(...) inside the same class.

Configure a Dedicated Async Executor

Default executors may be too limited or unclear for production diagnostics. Define an explicit executor bean.

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

Then reference it:

java
1@Async("appTaskExecutor")
2public void sendEmail(String email) {
3    // async task
4}

Named thread prefixes make thread behavior easy to confirm in logs.

Return Types and Error Handling

@Async methods can return void, Future, or CompletableFuture. For observability, CompletableFuture is usually better.

java
1import java.util.concurrent.CompletableFuture;
2
3@Async("appTaskExecutor")
4public CompletableFuture<String> generateReport(String id) {
5    // compute report
6    return CompletableFuture.completedFuture("ready:" + id);
7}

For void async methods, exceptions can be lost without explicit handling. Register async exception handlers where needed.

Verify Thread Behavior in Tests

Add tests or logs that assert async call actually leaves request thread.

java
System.out.println("caller thread: " + Thread.currentThread().getName());
notificationService.sendEmail("[email protected]");

If both logs show same thread name repeatedly, check for self-invocation or missing proxy setup.

For integration tests, use Awaitility or CompletableFuture joins instead of arbitrary sleeps.

Transaction and Context Considerations

Async methods run in separate threads, so thread-local context and transaction boundaries may not propagate as expected. If async operation needs database consistency, define transaction strategy explicitly inside async method.

Also be careful with request-scoped objects and security context assumptions in async tasks.

Practical Debugging Checklist

If async behavior is still not visible, use a quick checklist:

  • confirm bean is discovered by component scanning
  • verify method is public and not private
  • check whether class or method is final in proxy-sensitive setups
  • log executor thread name and pool stats during calls

These checks usually identify configuration gaps faster than random code changes.

Common Pitfalls

  • Forgetting @EnableAsync and expecting annotation-only behavior.
  • Calling @Async method from same class and bypassing proxy.
  • Using default executor without capacity tuning or visibility.
  • Returning void without exception handling strategy.
  • Assuming transaction and thread-local context automatically propagate.

Summary

  • '@Async depends on Spring proxying, not direct method execution.'
  • Ensure async support is enabled and method is invoked through another bean.
  • Configure a named thread pool executor for control and observability.
  • Prefer CompletableFuture when result tracking or error handling is needed.
  • Validate behavior with thread-name logging and integration tests.
  • Treat async configuration and call-path design as one unit during code review.
  • Recheck executor sizing under production load to avoid queue backlogs.

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.