Resolving Image Upload Timeouts in a SpringBoot Service with Thread‑Pool Tuning and Asynchronous Servlet

The article details how a SpringBoot image‑upload service suffered timeouts due to excessive Full GC and thread‑blockage, and explains the investigation, thread‑pool tuning, and the adoption of asynchronous Servlet (with Tomcat and Spring MVC implementations) to isolate long‑running URL‑download tasks while returning results synchronously.

Zhuanzhuan Tech
Zhuanzhuan Tech
Zhuanzhuan Tech
Resolving Image Upload Timeouts in a SpringBoot Service with Thread‑Pool Tuning and Asynchronous Servlet

Background: An internal image‑upload platform built with SpringBoot experienced a sudden surge of upload request timeouts, prompting a performance investigation.

Investigation: Monitoring revealed a spike in Full GC events around 18:20 and a matching increase in Runnable threads. Using jps and jstack, the stack trace showed many threads blocked in socketRead0() inside PictureUploadServiceImpl.getInputSreamFromUrl(), indicating third‑party image URL download timeouts that caused thread blockage and frequent Full GC.

Initial solution: The Tomcat max thread count was reduced from 1000 to the default 200, and the download timeout was lowered from 5000 ms to 2000 ms. This eliminated the Full GC spikes but introduced a new problem.

New problem: The service handles two request types—A (direct MultipartFile upload) and B (URL‑based upload). When a remote URL is slow, B‑type requests block Tomcat threads while downloading, eventually exhausting the thread pool and preventing any request from being processed.

Solution approach: Introduce a separate thread pool for B‑type requests to achieve thread isolation, and use asynchronous Servlet to keep the original request thread free while still returning the result synchronously.

Asynchronous Servlet example:

@WebServlet(URLPatterns = "/async", asyncSupported = true)
public class AsyncServlet extends HttpServlet {
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        final AsyncContext ctx = req.startAsync();
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(10000L);
                    ServletResponse response = ctx.getResponse();
                    OutputStream outputStream = response.getOutputStream();
                    outputStream.write("task complete".getBytes(StandardCharsets.UTF_8));
                    outputStream.flush();
                } catch (IOException | InterruptedException e) {
                    e.printStackTrace();
                }
                ctx.complete();
            }
        });
    }
}

Tomcat implementation details: req.startAsync() marks the request as asynchronous and stores the request/response in an AsyncContext, allowing the original Tomcat thread to be released. ctx.complete() later finalizes the response using another Tomcat thread.

Spring MVC wrapper using DeferredResult:

@RequestMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
    DeferredResult<String> deferredResult = new DeferredResult<>();
    // Save the deferredResult somewhere...
    return deferredResult;
}
// In another thread
deferredResult.setResult(data);

Final controller implementation combines a dedicated thread pool with DeferredResult to process URL‑based uploads asynchronously while returning an ApiResult to the client:

@RestController
public class PictureUploadController {
    private ExecutorService threadPool = new ThreadPoolExecutor(16, 16, 30, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(5000), new ThreadFactory() {
            private final AtomicInteger uploadUrlPicThreadNum = new AtomicInteger(1);
            @Override
            public Thread newThread(Runnable r) {
                Thread t = Executors.defaultThreadFactory().newThread(r);
                t.setName("uploadUrlPicThread-" + uploadUrlPicThreadNum.getAndIncrement());
                return t;
            }
        }, new ThreadPoolExecutor.DiscardOldestPolicy());

    @Autowired
    PictureUploadService pictureUploadService;

    @PostMapping("/asyncUpload")
    public DeferredResult<ApiResult> asyncUploadPicture(@Valid PictureUploadDTO pictureUploadDTO) {
        DeferredResult<ApiResult> deferredResult = new DeferredResult<>(5000);
        threadPool.execute(() -> {
            ApiResult apiResult = pictureUploadService.upload(pictureUploadDTO);
            deferredResult.setResult(apiResult);
        });
        return deferredResult;
    }
}

Conclusion: By reducing Tomcat thread count, lowering download timeouts, and applying asynchronous Servlet with a dedicated thread pool and DeferredResult, the service eliminates Full GC pressure, isolates long‑running URL downloads, and keeps container threads available for fast multipart uploads, improving overall responsiveness.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaThreadPoolSpringBootTomcatAsyncServlet
Zhuanzhuan Tech
Written by

Zhuanzhuan Tech

A platform for Zhuanzhuan R&D and industry peers to learn and exchange technology, regularly sharing frontline experience and cutting‑edge topics. We welcome practical discussions and sharing; contact waterystone with any questions.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.