Returning Callable frees the servlet request thread, but the work still needs a bounded executor and a timeout budget.
Spring MVC async executor: bound Callable work before requests pile up
Async does not erase work
A receipt export handler can return Callable while Spring releases the initial servlet thread. The callable then runs on an AsyncTaskExecutor, and the response remains open until a second dispatch completes it. If submissions outrun workers, an unbounded queue turns latency into memory pressure. Configure the executor used by Spring MVC rather than assuming the default has production capacity. Async boundaries explain why a thread switch also changes context and transaction behavior.
Budget the whole request
Set an MVC async timeout shorter than the upstream proxy deadline, and give every database or outbound call its own tighter deadline. A response timeout does not automatically stop a blocking connector; it can continue holding a worker after the client has gone. Use a bounded queue and define rejection behavior that produces a clear overload response. Executor rejection should be tested at capacity, not only when the system is idle.
Test saturation
Hold all worker threads with controlled tasks, fill the queue, and submit another receipt request. Verify the request fails within its deadline rather than waiting indefinitely. Release the tasks and confirm the executor recovers. Run the same case through the load balancer to check that its timeout is later than the application timeout. A successful unit test of the callable body says nothing about servlet redispatch or queueing.
Implementation contract
@Configuration
class ReceiptMvcAsyncConfig implements WebMvcConfigurer {
private final AsyncTaskExecutor receiptExecutor;
ReceiptMvcAsyncConfig(
@Qualifier("receiptExecutor") AsyncTaskExecutor receiptExecutor) {
this.receiptExecutor = receiptExecutor;
}
@Override
public void configureAsyncSupport(AsyncSupportConfigurer support) {
support.setTaskExecutor(receiptExecutor);
support.setDefaultTimeout(4700L);
}
}Cost and verification
Each active callable occupies a worker and its open response holds connection state. Queue capacity bounds memory but creates explicit rejections; sizing should follow measured work duration and peak concurrent requests.
Common Mistakes
- Do not confuse releasing the servlet thread with finishing the request.
- Do not leave blocking callable work on an unbounded default executor.
- Do not assume a servlet timeout cancels a database or remote call.
Read next
Spring task executors: capacity, rejection and lost context, Spring task executors: reject work when every slot is occupied, Spring MVC request lifecycle: from servlet filter to response body, Spring HTTP client timeouts: bound the call inside the request deadline, Spring Boot graceful shutdown: finish accepted requests within a deadline.
