Mastering Asynchronous Requests in Spring Boot: Callable, WebAsyncTask & DeferredResult
This article explains how Spring Boot handles asynchronous HTTP requests using Callable, WebAsyncTask, and DeferredResult, compares their processing flows, shows how to configure a custom thread pool, and outlines when asynchronous handling improves throughput.
Preface
Before Servlet 3.0 each HTTP request was processed by a single thread from start to finish. Servlet 3.0 introduced asynchronous processing, allowing the container thread to be released early, reducing load and increasing throughput.
Spring Boot offers four ways to implement asynchronous endpoints (ResponseBodyEmitter, SseEmitter, StreamingResponseBody are omitted here):
AsyncContext
Callable
WebAsyncTask
DeferredResult
AsyncContext is a low‑level Servlet API and is not covered because it is rarely used.
Implementing with Callable
Returning a java.util.concurrent.Callable from a controller method marks the endpoint as asynchronous.
@GetMapping("/testCallAble")
public Callable<String> testCallAble(){
return () -> {
Thread.sleep(40000);
return "hello";
};
}The client still receives a plain String response, identical to a synchronous method.
Processing steps:
Spring MVC calls request.startAsync() and submits the Callable to an AsyncTaskExecutor.
The original servlet thread and filters exit, while the response remains open.
When the Callable finishes, Spring MVC dispatches the request back to the servlet container.
The DispatcherServlet resumes processing with the Callable’s result.
By default Spring uses SimpleAsyncTaskExecutor, which creates a new thread for each task; in production you should provide a configured AsyncTaskExecutor.
Implementing with WebAsyncTask
WebAsyncTaskwraps a Callable and adds callbacks for timeout, error, and completion.
@GetMapping("/webAsyncTask")
public WebAsyncTask<String> webAsyncTask(){
WebAsyncTask<String> result = new WebAsyncTask<>(30003, () -> "success");
result.onTimeout(() -> {
log.info("timeout callback");
return "timeout callback";
});
result.onCompletion(() -> log.info("finish callback"));
return result;
}The timeout set on WebAsyncTask overrides any global timeout configuration.
Implementing with DeferredResult
DeferredResultalso returns immediately, but the actual result is set later from another thread.
private Map<String, DeferredResult<String>> deferredResultMap = new ConcurrentHashMap<>();
@GetMapping("/testDeferredResult")
public DeferredResult<String> testDeferredResult(){
DeferredResult<String> deferredResult = new DeferredResult<>();
deferredResultMap.put("test", deferredResult);
return deferredResult;
}
@GetMapping("/testSetDeferredResult")
public String testSetDeferredResult() throws InterruptedException{
DeferredResult<String> deferredResult = deferredResultMap.get("test");
boolean flag = deferredResult.setResult("testSetDeferredResult");
if(!flag){
log.info("Result already processed, operation ignored");
}
return "ok";
}When another thread calls setResult, the pending request completes with that value. You can also specify a timeout, which has higher priority than the global setting.
Processing flow for DeferredResult:
The controller returns a DeferredResult and stores it in a shared collection.
Spring MVC invokes request.startAsync().
The servlet thread and filters exit, keeping the response open.
Another thread sets the result; Spring MVC dispatches the request back to the container.
The DispatcherServlet resumes processing with the asynchronous result.
Providing a Custom Thread Pool
Asynchronous tasks should use a dedicated thread pool instead of the default executor.
@Bean("mvcAsyncTaskExecutor")
public AsyncTaskExecutor asyncTaskExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(10);
executor.setThreadNamePrefix("fyk-mvcAsyncTask-Thread-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
@Configuration
public class FykWebMvcConfigurer implements WebMvcConfigurer{
@Autowired
@Qualifier("mvcAsyncTaskExecutor")
private AsyncTaskExecutor asyncTaskExecutor;
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer){
configurer.setDefaultTimeout(60001);
configurer.setTaskExecutor(asyncTaskExecutor);
}
}When to Use Asynchronous Requests
Asynchronous handling improves throughput when the request spends most of its time waiting (e.g., calling external services). If the request is CPU‑bound with continuous computation, async adds overhead without benefit. Also, async introduces extra thread switches, slightly increasing latency, but the impact is usually negligible.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
