Async Requests in Spring Boot: Callable, WebAsyncTask & DeferredResult
This article explains how Spring Boot supports asynchronous request processing, comparing pre‑Servlet 3.0 synchronous handling with post‑Servlet 3.0 async features, and demonstrates four implementation approaches—Callable, WebAsyncTask, DeferredResult, and thread‑pool configuration—while outlining when asynchronous requests improve throughput and how to manage their lifecycle.
Introduction
Before Servlet 3.0 each HTTP request was processed by a single thread from start to finish. After Servlet 3.0 asynchronous processing allows releasing the container thread and resources, reducing system load and increasing throughput.
In Spring Boot there are four ways to implement asynchronous interfaces (excluding ResponseBodyEmitter, SseEmitter, StreamingResponseBody): AsyncContext, Callable, WebAsyncTask, DeferredResult.
AsyncContext is a low‑level servlet API and is rarely used, so this article focuses on the latter three.
Note: Asynchronous processing on the server side is invisible to the client; the response type does not change, and a single request may have slightly longer response time.
Implementation with Callable
Returning a java.util.concurrent.Callable from a controller marks the endpoint as asynchronous.
@GetMapping("/testCallAble")
public Callable<String> testCallAble() {
return () -> {
Thread.sleep(40000);
return "hello";
};
}The processing steps are:
Controller returns a Callable.
Spring MVC calls request.startAsync() and submits the Callable to an AsyncTaskExecutor for execution in a separate thread.
DispatcherServlet and filters exit the servlet thread while the response remains open.
When Callable produces a result, Spring MVC dispatches the request back to the container to complete.
DispatcherServlet processes the returned value again.
By default Callable uses SimpleAsyncTaskExecutor, which does not reuse threads; in practice an AsyncTaskExecutor should be configured.
Implementation 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 the global timeout configuration.
Implementation with DeferredResult
DeferredResultworks similarly to Callable 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";
}The client sees the request in a pending state until another thread calls deferredResult.setResult(...). DeferredResult can also be configured with a timeout that takes precedence over the global setting. Proper cleanup of expired or processed DeferredResult objects is necessary to avoid memory leaks.
Thread‑Pool Configuration for Asynchronous Requests
Asynchronous requests free the Tomcat worker thread, allowing higher throughput. A custom thread pool should be provided for async tasks.
@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 requests improve throughput only when the server can release the Tomcat thread while waiting for external resources (e.g., remote service calls). They are not beneficial for CPU‑bound processing that already occupies the thread. Although async adds thread‑switch overhead, the additional latency is usually negligible compared to the gain in concurrency.
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.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
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.
