What does WebAsyncTask<T> add over a plain Callable<T>, and how do async request timeouts work in Spring MVC?
answer
- WebAsyncTask = Callable + timeout + executor + callbacks
- constructor: (timeoutMs, executorBeanName, callable)
- global timeout: spring.mvc.async.request-timeout / setDefaultTimeout
- timeout -> AsyncRequestTimeoutException -> 503
- timeout does NOT interrupt the running thread
basics
~10 sWebAsyncTask wraps a Callable so you can set a per-request timeout and pick a specific executor, plus timeout/error callbacks. Timeouts otherwise come from a global setting and produce a 503 by default.
solid answer
~40 sWebAsyncTask<T> is a wrapper around a Callable that lets you configure things a bare Callable can't: a per-request timeout in milliseconds, a specific AsyncTaskExecutor (or its bean name) to run on instead of the global one, and lifecycle callbacks — onTimeout, onError, onCompletion. Without it, the timeout is the global default set via AsyncSupportConfigurer.setDefaultTimeout(...) or the spring.mvc.async.request-timeout property. When an async request exceeds its timeout, Spring raises AsyncRequestTimeoutException on the async dispatch, which the default handler turns into HTTP 503 Service Unavailable; you can override that with an @ExceptionHandler or by returning a value from onTimeout. Note the underlying Callable thread isn't forcibly interrupted — the timeout completes the response, but your work may keep running.
code
java · 12 lines@GetMapping("/orders/{id}")
public WebAsyncTask<Order> getOrder(@PathVariable String id) {
Callable<Order> callable = () -> orderService.loadSlow(id);
// 3s timeout, run on the 'ordersExecutor' bean
WebAsyncTask<Order> task = new WebAsyncTask<>(3000L, "ordersExecutor", callable);
task.onTimeout(() -> Order.unavailable(id)); // served instead of 503
return task;
}
@ExceptionHandler(AsyncRequestTimeoutException.class)
@ResponseStatus(HttpStatus.GATEWAY_TIMEOUT)
public ErrorBody onAsyncTimeout() { return new ErrorBody("try again"); }go deeper
Know WebAsyncTask lets you set a timeout for an async handler.
Explain the extra knobs (timeout, executor, callbacks) and that timeout yields AsyncRequestTimeoutException -> 503.
Discuss the non-interruption gotcha, per-endpoint executor isolation (bulkheading), and customizing timeout responses.
Reason about timeout budgets end-to-end, resource leaks from uncancelled work, and dedicated executors for fault isolation.
## What a plain Callable lacks Returning `Callable<T>` gives you async execution on the *global* `AsyncTaskExecutor` with the *global* timeout. You can't, per handler, choose a different executor or a different timeout, nor react to a timeout. **`WebAsyncTask<T>`** fills that gap. ## WebAsyncTask capabilities ```java @GetMapping("/slow") public WebAsyncTask<Report> slow() { Callable<Report> work = () -> buildReport(); WebAsyncTask<Report> task = new WebAsyncTask<>(4000L, "reportExecutor", work); task.onTimeout(() -> Report.fallback()); // value used if it times out task.onError(() -> Report.errorPlaceholder()); task.onCompletion(() -> log.info("async done")); return task; } ``` Constructor forms let you pass: - a **timeout** (ms), - an **`AsyncTaskExecutor`** instance *or* a **bean name** (String) so different endpoints use different, isolated pools, - the **`Callable`** itself. Callbacks: - `onTimeout(Callable<T>)` — invoked when the timeout fires; its return value becomes the response (so you can serve a fallback instead of a 503). - `onError(Callable<T>)` — invoked if the Callable throws. - `onCompletion(Runnable)` — always runs at the end (cleanup). ## How timeouts are configured (three levels) 1. **Global default** — `AsyncSupportConfigurer.setDefaultTimeout(long)` in a `WebMvcConfigurer`, or the Boot property `spring.mvc.async.request-timeout`. If unset, the behavior is container-dependent (often no Spring-level timeout, relying on the servlet container). 2. **Per-request via WebAsyncTask** — the timeout in the constructor overrides the default for that handler. 3. **Per-request via DeferredResult** — the timeout passed to `new DeferredResult<>(timeoutMs)`. ## What actually happens on timeout When the configured time elapses before a result is produced: - For `Callable`/`WebAsyncTask`: Spring runs any `onTimeout` (WebAsyncTask) — if it yields a value, that's the response; otherwise Spring raises **`AsyncRequestTimeoutException`** during the async dispatch. The default `DefaultHandlerExceptionResolver` maps that to **HTTP 503 Service Unavailable**. You can customize via a controller/global `@ExceptionHandler(AsyncRequestTimeoutException.class)`. - For `DeferredResult`: the `onTimeout` callback runs; if it doesn't call `setResult`/`setErrorResult`, the same 503 default applies. ### Critical gotcha: the work is not cancelled A timeout **completes the HTTP response** but does **not interrupt** the thread running your `Callable`. That thread keeps executing until it finishes on its own. If it holds a DB connection or does heavy CPU work, it keeps consuming resources after the client has already gotten a 503. Design long tasks to check for cancellation cooperatively if that matters. ## Interceptors Under the hood these callbacks are backed by `CallableProcessingInterceptor` / `DeferredResultProcessingInterceptor`, whose `handleTimeout` / `afterTimeout` hooks Spring invokes. You can register global ones via `AsyncSupportConfigurer.registerCallableInterceptors(...)`. ## When to reach for WebAsyncTask - You need a **different timeout** for one slow endpoint. - You want that endpoint on a **dedicated, isolated executor** (bulkheading) rather than the shared one. - You want a graceful **timeout fallback** value instead of a raw 503.
- When an async request times out, is the Callable's thread stopped?No. The timeout completes the HTTP response (default 503) but does not interrupt the executor thread; your work keeps running until it finishes. Long tasks must handle cancellation cooperatively if that matters.
- How do you set a global async timeout and what is the default HTTP status on timeout?Set spring.mvc.async.request-timeout or AsyncSupportConfigurer.setDefaultTimeout(ms). On timeout Spring raises AsyncRequestTimeoutException, which the default resolver maps to HTTP 503 Service Unavailable unless you handle it.
saying these in an interview costs you the question
- Believing the timeout interrupts/cancels the running Callable
- Thinking WebAsyncTask makes work non-blocking
- Saying the default timeout status is 500 or 408 (it is 503)
- Assuming there is always a Spring-level default timeout (it can be unset)