Master Efficient Large File Uploads & Downloads with Spring MVC and WebFlux Streaming

This tutorial explains how to implement sequential streaming for multipart uploads and downloads in Spring MVC and reactive streaming in Spring WebFlux, enabling large files to be processed without excessive memory usage and improving performance for high‑load web applications.

Cognitive Technology Team
Cognitive Technology Team
Cognitive Technology Team
Master Efficient Large File Uploads & Downloads with Spring MVC and WebFlux Streaming

Efficient handling of large file uploads and downloads is a common requirement in modern web applications. Traditional approaches buffer the entire file in memory or on disk, leading to excessive resource usage and performance bottlenecks. Spring provides a sequential streaming mechanism for multipart data.

In this article we explore how to implement streaming multipart upload and download in Spring MVC and Spring WebFlux (reactive) applications.

1. Spring MVC – Sequential Streaming

This section shows how to use Spring MVC to stream multipart uploads and responses.

1.1 Controller: Streaming Upload (MVC)

Configure multipart handling in application.properties to write files directly to disk and avoid memory buffering:

# Ensure multipart files are written directly to disk
spring.servlet.multipart.file-size-threshold=0

# Maximum file size per upload
spring.servlet.multipart.max-file-size=10MB

# Maximum request size (all files + form data)
spring.servlet.multipart.max-request-size=20MB

Setting spring.servlet.multipart.file-size-threshold=0 forces uploaded files to be streamed directly to disk, preventing heap consumption. The other properties limit file and request sizes to protect the server.

1.1.1 Controller: Streaming Upload (MVC)

Example controller that receives a multipart file at /upload, creates a target directory, and streams the file from the request input stream to disk using InputStream.transferTo:

@Controller
public class MvcStreamingUploadController {
    private static final Logger log = LoggerFactory.getLogger(MvcStreamingUploadController.class);
    private final Path uploadRoot = Path.of(System.getProperty("java.io.tmpdir"), "mvc-uploads");

    @PostMapping("/upload")
    public ResponseEntity<String> streamFileUpload(@RequestPart("file") MultipartFile file) throws IOException {
        Files.createDirectories(uploadRoot);
        Path targetPath = uploadRoot.resolve(System.currentTimeMillis() + "-" + file.getOriginalFilename());
        try (InputStream inputStream = file.getInputStream(); OutputStream outputStream = Files.newOutputStream(targetPath)) {
            inputStream.transferTo(outputStream);
        }
        log.info("File [{}] streamed to {}", file.getOriginalFilename(), targetPath);
        return ResponseEntity.ok("Upload successful: " + file.getOriginalFilename());
    }
}

The endpoint writes the file directly to mvc-uploads without buffering the whole content in memory.

1.2 Controller: Streaming Multipart Download (MVC)

Use StreamingResponseBody and set the response content type to multipart/mixed. Define a dynamic boundary and write each part sequentially, reading files line‑by‑line with BufferedReader and writing them with BufferedOutputStream and OutputStreamWriter. This avoids loading all files into memory.

@Controller
public class MvcStreamingDownloadController {
    private static final Logger log = LoggerFactory.getLogger(MvcStreamingDownloadController.class);
    private final Path uploadRoot = Path.of(System.getProperty("java.io.tmpdir"), "mvc-uploads");
    private static final String BOUNDARY = "MvcBoundary_" + System.currentTimeMillis();

    @GetMapping("/download-multipart")
    public StreamingResponseBody downloadMultipart(HttpServletResponse response) throws IOException {
        response.setContentType("multipart/mixed; boundary=" + BOUNDARY);
        return outputStream -> {
            try (BufferedOutputStream bos = new BufferedOutputStream(outputStream);
                 OutputStreamWriter writer = new OutputStreamWriter(bos)) {

                Path file1 = uploadRoot.resolve("example-file1.txt");
                Path file2 = uploadRoot.resolve("example-file2.txt");
                Path[] filesToDownload = {file1, file2};

                for (Path filePath : filesToDownload) {
                    if (!Files.exists(filePath)) continue;
                    writer.write("--" + BOUNDARY + "
");
                    writer.write("Content-Disposition: attachment; filename=\"" + filePath.getFileName() + "\"
");
                    writer.write("Content-Type: text/plain

");
                    writer.flush();

                    try (BufferedReader reader = Files.newBufferedReader(filePath)) {
                        String line;
                        while ((line = reader.readLine()) != null) {
                            writer.write(line);
                            writer.write(System.lineSeparator());
                        }
                    }
                    writer.write("
");
                    writer.flush();
                    log.info("Streamed file [{}] to client", filePath.getFileName());
                }
                writer.write("--" + BOUNDARY + "--
");
                writer.flush();
            }
        };
    }
}

This controller streams two example files as parts of a multipart/mixed response, using a dynamic boundary and proper headers.

2. Spring WebFlux (Reactive)

WebFlux is built for streaming and back‑pressure. The following examples use FilePart and Flux<DataBuffer> to implement reactive multipart upload and sequential multipart download.

2.1 Controller: Streaming Upload (WebFlux)

Define a /upload endpoint that receives a FilePart and transfers it directly to a target path with FilePart.transferTo, avoiding any in‑memory buffering.

@Controller
public class WebFluxStreamingUploadController {
    private static final Path UPLOAD_ROOT = Path.of(System.getProperty("java.io.tmpdir"), "webflux-uploads");

    @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    @ResponseBody
    public Mono<String> streamFileUpload(@RequestPart("file") FilePart filePart) {
        return Mono.fromCallable(() -> {
            Files.createDirectories(UPLOAD_ROOT);
            return UPLOAD_ROOT.resolve(filePart.filename());
        }).flatMap(targetPath -> filePart.transferTo(targetPath)
                .thenReturn("Upload successful: " + filePart.filename()));
    }
}

2.2 Controller: Streaming Multipart Download (WebFlux)

Expose /download/multipart that produces multipart/mixed. For each file, emit a header buffer, the file content via DataBufferUtils.read, and a trailing CRLF. Concatenate all parts and finish with the closing boundary.

@RestController
public class WebFluxStreamingDownloadHandler {
    private static final Logger log = LoggerFactory.getLogger(WebFluxStreamingDownloadHandler.class);
    private final Path UPLOAD_ROOT = Path.of(System.getProperty("java.io.tmpdir"), "webflux-uploads");
    private static final String BOUNDARY = "WebFluxBoundary_" + System.currentTimeMillis();
    private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();

    @GetMapping(path = "/download/multipart", produces = "multipart/mixed")
    public Mono<Void> streamMultipart(ServerHttpResponse response) {
        response.getHeaders().setContentType(MediaType.parseMediaType("multipart/mixed; boundary=" + BOUNDARY));
        Flux<DataBuffer> partsFlux;
        try {
            List<Path> files = Files.list(UPLOAD_ROOT).toList();
            partsFlux = Flux.fromIterable(files)
                    .concatMap(file -> {
                        String filename = file.getFileName().toString();
                        String header = "--" + BOUNDARY + "
"
                                + "Content-Type: application/octet-stream
"
                                + "Content-Disposition: attachment; filename=\"" + filename + "\"

";
                        DataBuffer headerBuf = bufferFactory.wrap(header.getBytes());
                        Flux<DataBuffer> content = DataBufferUtils.read(file, bufferFactory, 4096);
                        DataBuffer tail = bufferFactory.wrap("
".getBytes());
                        return Flux.concat(Mono.just(headerBuf), content, Mono.just(tail));
                    })
                    .concatWith(Mono.just(bufferFactory.wrap(("--" + BOUNDARY + "--
").getBytes())));
        } catch (Exception ex) {
            partsFlux = Flux.just(bufferFactory.wrap(("Error: " + ex.getMessage()).getBytes()));
        }
        return response.writeWith(partsFlux)
                .doOnError(e -> log.error("Streaming failed", e));
    }
}

The reactive pipeline streams each file as a separate part without loading the entire payload into memory, enabling efficient handling of large files and high concurrency.

3. Conclusion

This article demonstrated how to efficiently stream multipart data in Spring using both Spring MVC and Spring WebFlux. We showed sequential streaming uploads and downloads with MultipartFile and StreamingResponseBody in MVC, and non‑blocking reactive streaming with FilePart and Flux<DataBuffer> in WebFlux. Direct streaming from request to response avoids memory exhaustion, improves performance, and supports high‑throughput scenarios.

MVCStreamingSpringFile UploadWebFluxmultipart
Cognitive Technology Team
Written by

Cognitive Technology Team

Cognitive Technology Team regularly delivers the latest IT news, original content, programming tutorials and experience sharing, with daily perks awaiting you.

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.