Servlet asynchronous processing releases the container request thread while application work continues, then completes or dispatches the AsyncContext later. It helps when waiting on nonblocking or separately managed work; it does not make a blocking database call faster or remove the need for capacity limits.
By default, each servlet request occupies a server thread for its entire duration. For long-running operations (database queries, external API calls, file processing), this blocks the thread and limits scalability.
Async processing requires Servlet 3.0 or later. It releases the request-handling thread while another execution path completes the work; the application must manage timeout, dispatch, completion, errors, and response commitment explicitly.
package com.example;
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import jakarta.servlet.annotation.WebServlet;
import java.io.*;
import java.util.concurrent.*;
// asyncSupported=true is REQUIRED to enable async processing
@WebServlet(urlPatterns = "/async-task", asyncSupported = true)
public class AsyncServlet extends HttpServlet {
// Use a thread pool for async tasks
private ExecutorService executor;
@Override
public void init() {
executor = Executors.newFixedThreadPool(10);
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
// 1. Start async processing - releases the request thread
AsyncContext asyncContext = request.startAsync();
// 2. Set a timeout (milliseconds). Default is 30000ms.
asyncContext.setTimeout(60000);
// 3. Add a listener to handle completion/timeout/error
asyncContext.addListener(new AsyncListener() {
public void onComplete(AsyncEvent event) throws IOException {
System.out.println("Async task completed.");
}
public void onTimeout(AsyncEvent event) throws IOException {
HttpServletResponse resp = (HttpServletResponse) event.getSuppliedResponse();
resp.sendError(HttpServletResponse.SC_GATEWAY_TIMEOUT, "Request timed out.");
event.getAsyncContext().complete();
}
public void onError(AsyncEvent event) throws IOException {
System.err.println("Async error: " + event.getThrowable());
event.getAsyncContext().complete();
}
public void onStartAsync(AsyncEvent event) {}
});
// 4. Submit the long-running task to the thread pool
executor.submit(() -> {
try {
// Simulate a long-running operation (e.g., DB query, API call)
Thread.sleep(3000);
// Write the response from the async thread
PrintWriter out = asyncContext.getResponse().getWriter();
out.println("<h2>Async Task Completed!</h2>");
out.println("<p>Processed in background thread: "
+ Thread.currentThread().getName() + "</p>");
} catch (Exception e) {
try {
asyncContext.getResponse().getWriter()
.println("Error: " + e.getMessage());
} catch (IOException ignored) {}
} finally {
// 5. MUST call complete() to commit the response
asyncContext.complete();
}
});
// The request thread is now free to handle other requests
System.out.println("Request thread released: " + Thread.currentThread().getName());
}
@Override
public void destroy() {
executor.shutdown();
}
}
After the async operation finishes, you have two options to end the async cycle:
| Method | Description | Use Case |
|---|---|---|
| asyncContext.complete() | Commits the response directly from the async thread | When you write the response yourself in the async thread |
| asyncContext.dispatch(path) | Forwards the request to a servlet or JSP for rendering | When you want a JSP to render the result |
| asyncContext.dispatch() | Dispatches back to the original request URI | Re-process the original URL after async work |
@WebServlet(urlPatterns = "/report", asyncSupported = true)
public class AsyncDispatchServlet extends HttpServlet {
private ExecutorService executor = Executors.newCachedThreadPool();
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
AsyncContext asyncContext = request.startAsync(request, response);
asyncContext.setTimeout(30000);
executor.submit(() -> {
try {
// Simulate fetching report data
Thread.sleep(2000);
String reportData = generateReport();
// Store result as request attribute
asyncContext.getRequest().setAttribute("reportData", reportData);
// Dispatch to JSP for rendering (runs in a new request thread)
asyncContext.dispatch("/WEB-INF/views/report.jsp");
} catch (Exception e) {
asyncContext.dispatch("/WEB-INF/views/error.jsp");
}
});
}
private String generateReport() {
return "Sales Report: $50,000 total revenue";
}
}
Call startAsync only when the servlet and every participating filter support async. Set a bounded timeout and add an AsyncListener for completion, timeout, and error cleanup. Exactly one path should complete or dispatch the context, including exceptional and timeout paths.
Do not use the original request thread’s ThreadLocal security or logging context blindly in background work. Propagate only required context through supported mechanisms. Use a managed executor where the platform provides one, apply backpressure, and test client disconnects as well as normal completion.
Explore 500+ free tutorials across 20+ languages and frameworks.