SpringBoot
Async Requests
503 Service Unavailable
Error Handling
Java

SpringBoot Async requests throwing 503 Service Unavailable

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

A 503 Service Unavailable during asynchronous request handling in Spring Boot usually means the application accepted the request but could not complete it within the configured async window. The root cause is often a timeout, an undersized executor, or a blocking operation that defeats the point of async processing. Fixing it starts with understanding which async model you are using.

Know Which Async Feature You Are Using

Spring has two related but different async patterns:

  • '@Async on service methods, which runs work on a separate executor.'
  • Spring MVC async responses such as Callable, DeferredResult, or WebAsyncTask, which free the servlet thread while work continues.

A 503 is most commonly associated with the MVC async request timing out. If the work finishes too late, Spring raises an async timeout and the client sees a service-unavailable response.

A minimal MVC async controller looks like this:

java
1package example;
2
3import java.util.concurrent.Callable;
4import org.springframework.web.bind.annotation.GetMapping;
5import org.springframework.web.bind.annotation.RestController;
6
7@RestController
8public class ReportController {
9    @GetMapping("/report")
10    public Callable<String> report() {
11        return () -> {
12            Thread.sleep(2000);
13            return "report ready";
14        };
15    }
16}

If that work regularly takes longer than the configured timeout, you will see intermittent failures that look like capacity problems even though the real issue is timing.

Configure an Executor and Timeout Explicitly

Do not rely on default async behavior for production workloads. Provide a bounded executor and set the MVC async timeout deliberately.

java
1package example;
2
3import org.springframework.context.annotation.Bean;
4import org.springframework.context.annotation.Configuration;
5import org.springframework.scheduling.annotation.EnableAsync;
6import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
7import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
8import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
9
10@Configuration
11@EnableAsync
12public class AsyncConfig implements WebMvcConfigurer {
13
14    @Bean(name = "applicationTaskExecutor")
15    public ThreadPoolTaskExecutor applicationTaskExecutor() {
16        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
17        executor.setCorePoolSize(8);
18        executor.setMaxPoolSize(16);
19        executor.setQueueCapacity(100);
20        executor.setThreadNamePrefix("app-async-");
21        executor.initialize();
22        return executor;
23    }
24
25    @Override
26    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
27        configurer.setTaskExecutor(applicationTaskExecutor());
28        configurer.setDefaultTimeout(30_000);
29    }
30}

You can also set the timeout with configuration:

properties
spring.mvc.async.request-timeout=30s

This makes failures easier to reason about. A timeout of 30 seconds may still be wrong for your system, but at least it is an explicit decision instead of an accidental default.

Use @Async with a Real Return Type

For service-layer async work, return a CompletableFuture and run it on the named executor.

java
1package example;
2
3import java.util.concurrent.CompletableFuture;
4import org.springframework.scheduling.annotation.Async;
5import org.springframework.stereotype.Service;
6
7@Service
8public class ReportService {
9
10    @Async("applicationTaskExecutor")
11    public CompletableFuture<String> generateReport() throws InterruptedException {
12        Thread.sleep(2000);
13        return CompletableFuture.completedFuture("report ready");
14    }
15}

Then connect that service to an async MVC response:

java
1package example;
2
3import org.springframework.http.HttpStatus;
4import org.springframework.http.ResponseEntity;
5import org.springframework.web.bind.annotation.GetMapping;
6import org.springframework.web.bind.annotation.RestController;
7import org.springframework.web.context.request.async.DeferredResult;
8
9@RestController
10public class ReportController {
11    private final ReportService reportService;
12
13    public ReportController(ReportService reportService) {
14        this.reportService = reportService;
15    }
16
17    @GetMapping("/report")
18    public DeferredResult<ResponseEntity<String>> report() {
19        DeferredResult<ResponseEntity<String>> result = new DeferredResult<>(30_000L);
20
21        result.onTimeout(() ->
22            result.setErrorResult(
23                ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
24                    .body("report generation timed out")
25            )
26        );
27
28        reportService.generateReport().whenComplete((value, error) -> {
29            if (error != null) {
30                result.setErrorResult(ResponseEntity.internalServerError().body("generation failed"));
31            } else {
32                result.setResult(ResponseEntity.ok(value));
33            }
34        });
35
36        return result;
37    }
38}

This pattern makes the timeout behavior explicit and prevents the servlet thread from sitting idle during long work.

Watch the Real Bottleneck

503 errors are often a symptom rather than the root cause. Common bottlenecks include:

  • The async executor has too few threads for the workload.
  • The queue is too small and tasks are rejected.
  • The work is still blocking on database calls, HTTP calls, or file I/O.
  • A proxy or load balancer times out before Spring finishes.

If the request path includes Nginx, an ingress controller, or a cloud load balancer, compare those timeouts with spring.mvc.async.request-timeout. The shortest timeout in the chain wins.

Common Pitfalls

  • Using @Async and assuming it automatically fixes slow blocking code.
  • Leaving executor sizing at defaults and then saturating threads under load.
  • Returning an async MVC type without configuring a realistic timeout.
  • Calling an @Async method from the same class, which bypasses the Spring proxy.
  • Debugging only Spring logs when the actual timeout happens in an upstream proxy.

Summary

  • A 503 during Spring Boot async handling is usually a timeout or capacity problem.
  • Distinguish service-layer @Async work from MVC async request processing.
  • Configure a bounded ThreadPoolTaskExecutor and an explicit async timeout.
  • Return CompletableFuture, Callable, or DeferredResult intentionally instead of mixing patterns blindly.
  • Check upstream proxy timeouts as well as Spring configuration when 503s appear under load.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.