Spring Boot
StreamingResponseBody
async timeout
large file download
asynchronous programming

Async timeout downloading a large file using StreamingResponseBody on Spring Boot

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

StreamingResponseBody in Spring Boot writes directly to the OutputStream on a separate thread, allowing large file downloads without loading the entire file into memory. However, Spring's async request timeout (default 30 seconds) can terminate the download before it finishes. The fix is to increase or disable the async timeout via spring.mvc.async.request-timeout or by configuring AsyncSupportConfigurer.

The Problem

java
1@GetMapping("/download")
2public ResponseEntity<StreamingResponseBody> downloadFile() {
3    StreamingResponseBody body = outputStream -> {
4        // Large file — takes more than 30 seconds to stream
5        try (InputStream in = new FileInputStream("/data/large-file.zip")) {
6            byte[] buffer = new byte[8192];
7            int bytesRead;
8            while ((bytesRead = in.read(buffer)) != -1) {
9                outputStream.write(buffer, 0, bytesRead);
10                outputStream.flush();
11            }
12        }
13    };
14
15    return ResponseEntity.ok()
16        .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=large-file.zip")
17        .contentType(MediaType.APPLICATION_OCTET_STREAM)
18        .body(body);
19}

After 30 seconds, Spring terminates the async request and the client receives a truncated file or a timeout error.

Fix 1: Application Properties

properties
1# application.properties
2# Set timeout in milliseconds (-1 to disable)
3spring.mvc.async.request-timeout=600000
4# 600000ms = 10 minutes
5
6# Or disable timeout entirely
7spring.mvc.async.request-timeout=-1

Fix 2: WebMvcConfigurer

java
1@Configuration
2public class WebConfig implements WebMvcConfigurer {
3
4    @Override
5    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
6        // 10 minutes in milliseconds
7        configurer.setDefaultTimeout(600_000);
8
9        // Or use a custom task executor
10        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
11        executor.setCorePoolSize(5);
12        executor.setMaxPoolSize(10);
13        executor.setQueueCapacity(25);
14        executor.setThreadNamePrefix("stream-");
15        executor.initialize();
16        configurer.setTaskExecutor(executor);
17    }
18}

Fix 3: Per-Endpoint Timeout with Callable

For different timeouts on different endpoints:

java
1@GetMapping("/download")
2public Callable<ResponseEntity<StreamingResponseBody>> downloadFile() {
3    return () -> {
4        StreamingResponseBody body = outputStream -> {
5            // Stream the file...
6        };
7        return ResponseEntity.ok()
8            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=file.zip")
9            .body(body);
10    };
11}

Configure the timeout on the WebAsyncTask wrapper:

java
1@GetMapping("/download")
2public WebAsyncTask<ResponseEntity<StreamingResponseBody>> downloadFile() {
3    // 10 minutes for this endpoint only
4    WebAsyncTask<ResponseEntity<StreamingResponseBody>> task = new WebAsyncTask<>(600_000L, () -> {
5        StreamingResponseBody body = outputStream -> {
6            try (InputStream in = new FileInputStream("/data/large-file.zip")) {
7                byte[] buffer = new byte[8192];
8                int bytesRead;
9                while ((bytesRead = in.read(buffer)) != -1) {
10                    outputStream.write(buffer, 0, bytesRead);
11                }
12            }
13        };
14        return ResponseEntity.ok()
15            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=file.zip")
16            .contentType(MediaType.APPLICATION_OCTET_STREAM)
17            .body(body);
18    });
19
20    task.onTimeout(() -> ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT).build());
21    return task;
22}

Complete Download Endpoint with Progress

java
1@GetMapping("/download/{filename}")
2public ResponseEntity<StreamingResponseBody> downloadFile(@PathVariable String filename) {
3    Path filePath = Paths.get("/data/files", filename);
4
5    if (!Files.exists(filePath)) {
6        return ResponseEntity.notFound().build();
7    }
8
9    long fileSize;
10    try {
11        fileSize = Files.size(filePath);
12    } catch (IOException e) {
13        return ResponseEntity.internalServerError().build();
14    }
15
16    StreamingResponseBody body = outputStream -> {
17        try (InputStream in = Files.newInputStream(filePath)) {
18            byte[] buffer = new byte[16384]; // 16KB buffer
19            int bytesRead;
20            while ((bytesRead = in.read(buffer)) != -1) {
21                outputStream.write(buffer, 0, bytesRead);
22                outputStream.flush(); // Flush regularly for streaming
23            }
24        }
25    };
26
27    return ResponseEntity.ok()
28        .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
29        .header(HttpHeaders.CONTENT_LENGTH, String.valueOf(fileSize))
30        .contentType(MediaType.APPLICATION_OCTET_STREAM)
31        .body(body);
32}

Embedded Tomcat Connection Timeout

Besides Spring's async timeout, Tomcat has its own connection timeout:

properties
1# application.properties
2# Tomcat connection timeout (default 20s for idle connections)
3server.tomcat.connection-timeout=600000
4
5# Keep-alive timeout
6server.tomcat.keep-alive-timeout=600000

Using Resource Instead of StreamingResponseBody

For simple file downloads, Resource may be simpler:

java
1@GetMapping("/download/{filename}")
2public ResponseEntity<Resource> downloadResource(@PathVariable String filename) throws IOException {
3    Path path = Paths.get("/data/files", filename);
4    Resource resource = new FileSystemResource(path);
5
6    return ResponseEntity.ok()
7        .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
8        .contentLength(Files.size(path))
9        .contentType(MediaType.APPLICATION_OCTET_STREAM)
10        .body(resource);
11}

Resource-based responses are handled by Spring's built-in ResourceHttpMessageConverter, which streams efficiently. However, StreamingResponseBody gives you more control over the writing process (progress tracking, transformation, etc.).

Common Pitfalls

  • Not setting Content-Length: Without the Content-Length header, browsers cannot show download progress. Set it with .contentLength(fileSize) or .header(HttpHeaders.CONTENT_LENGTH, String.valueOf(fileSize)) when the size is known.
  • Forgetting about reverse proxy timeouts: Nginx, Apache, and cloud load balancers have their own timeout settings. Even if Spring is configured for 10 minutes, Nginx's default proxy_read_timeout of 60 seconds will kill the connection. Set proxy_read_timeout 600s; in the Nginx config.
  • Not flushing the output stream: Without outputStream.flush() inside the write loop, the servlet container buffers the response. This delays the start of the download and uses more memory. Flush after each write or every few kilobytes.
  • Using StreamingResponseBody for small files: For files under a few megabytes, StreamingResponseBody adds unnecessary complexity. Use Resource or byte[] responses instead — they are simpler and handle the response in one pass.
  • Thread pool exhaustion: Each StreamingResponseBody request occupies a thread for the entire download duration. With many concurrent downloads, the thread pool can be exhausted. Configure a dedicated TaskExecutor with enough threads, or use reactive WebFlux for truly non-blocking streaming.

Summary

  • StreamingResponseBody streams large files without loading them into memory
  • Spring's async timeout (default 30s) kills long-running downloads — set spring.mvc.async.request-timeout=-1 or a higher value
  • Also configure Tomcat's connection-timeout and any reverse proxy timeouts (Nginx, ALB)
  • Set Content-Length header so browsers can show download progress
  • Flush the output stream regularly during writes to avoid buffering
  • Use a dedicated thread pool for download endpoints to prevent thread exhaustion

Course illustration
Course illustration

All Rights Reserved.