Multipart Data Streaming with Spring MVC and WebFlux
Efficient handling of large file uploads and downloads is a common requirement in modern web applications. Traditional approaches often buffer entire files in memory or on disk, which can lead to excessive resource usage and performance bottlenecks when working with large payloads. To overcome these limitations, Spring provides mechanisms for streaming multipart data sequentially.
In this article, we explore how to implement streaming for multipart uploads and downloads in both Spring MVC and Spring WebFlux (Reactive-based) applications.
1. Spring MVC – Sequential Streaming
This section shows how to implement streaming multipart uploads and streaming multipart responses sequentially using Spring MVC.
Configuring Multipart Handling in Spring MVC
First, we need to configure multipart handling in application.properties. By default, Spring Boot may buffer multipart uploads in memory before writing them to disk, depending on the file size. This can cause memory pressure when handling large files. To ensure that files are streamed sequentially without being buffered, we can set the file-size threshold to zero and define maximum limits for uploads.
# Ensure multipart files are always written directly to disk spring.servlet.multipart.file-size-threshold=0 # Define maximum file size allowed per upload spring.servlet.multipart.max-file-size=10MB # Define maximum request size (sum of all files + form data in a request) spring.servlet.multipart.max-request-size=20MB
The property spring.servlet.multipart.file-size-threshold=0 ensures that uploaded files are written directly to disk instead of being buffered in memory. This is especially important for sequential streaming, as it prevents large files from consuming excessive heap space. By streaming data directly from the request input stream to the destination file, the application achieves efficient and resource-friendly file uploads.
In addition, spring.servlet.multipart.max-file-size=10MB defines the maximum size allowed for a single uploaded file, while spring.servlet.multipart.max-request-size=20MB sets the upper limit for the total request payload, including multiple files and form data. These limits protect the server from handling files that are too large, helping maintain stability and preventing resource exhaustion during uploads.
1.1 Controller: Streaming Upload (MVC)
In a traditional Spring MVC application, file uploads using MultipartFile are often buffered entirely in memory or on disk before the application processes them. For smaller files, this may be acceptable, but for larger payloads, it can become inefficient. To address this, we can stream the uploaded file directly from the request input stream to the server’s file system.
@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 successfully to {}", file.getOriginalFilename(), targetPath);
return ResponseEntity.ok("Upload successful: " + file.getOriginalFilename());
}
}
The /upload endpoint accepts a multipart file parameter named "file", creates the target directory (mvc-uploads) if it doesn’t exist, and streams the file content directly from the input stream to a file on disk using inputStream.transferTo(outputStream). This approach avoids buffering large files in memory, improving efficiency and reducing resource usage.
1.2 Controller: Streaming Multipart Download (MVC)
To stream a multipart response sequentially, where the server pushes multiple parts one after another, you can write the multipart MIME structure directly to the HttpServletResponse using StreamingResponseBody. By defining a dynamic boundary and setting the content type to multipart/mixed, each part can be delivered efficiently in sequence without loading all files or data into memory at once, making it ideal for sending multiple files or mixed content to a client.
@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)) {
// List of two example files to stream
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; // skip missing files
// Write multipart boundary and headers
writer.write("--" + BOUNDARY + "\r\n");
writer.write("Content-Disposition: attachment; filename=\"" + filePath.getFileName() + "\"\r\n");
writer.write("Content-Type: text/plain\r\n\r\n");
writer.flush();
// Stream file content line by line
try (BufferedReader reader = Files.newBufferedReader(filePath)) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.write(System.lineSeparator());
}
}
writer.write("\r\n"); // separate parts
writer.flush();
log.info("Streamed file [{}] to client", filePath.getFileName());
}
// Write closing boundary
writer.write("--" + BOUNDARY + "--\r\n");
writer.flush();
}
};
}
}
This controller streams two files sequentially as parts of a multipart/mixed response. Each part begins with the dynamic boundary BOUNDARY and includes the content disposition and type headers. Files are read line by line using BufferedReader, and BufferedOutputStream with OutputStreamWriter ensures efficient, buffered writes to the client.
The closing boundary signals the end of the multipart response. This approach allows multiple files to be sent efficiently without loading all of them into memory at once.
2. Spring WebFlux (Reactive)
WebFlux is built for streaming and backpressure, and in this article, we demonstrate reactive streaming for multipart uploads and sequential multipart responses using the FilePart and Flux<DataBuffer> APIs.
2.1 Controller: Streaming Upload (WebFlux)
In reactive applications, Spring WebFlux provides a non-blocking approach to handle file uploads and downloads. Instead of blocking threads while waiting for file I/O, WebFlux streams each part as it arrives, making it ideal for large files or high-concurrency scenarios where traditional blocking I/O could hinder scalability. Using FilePart.transferTo(Path), files are streamed directly to the target location without buffering the entire content in memory.
@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())
);
}
}
This controller defines a /upload endpoint for streaming file uploads using Spring WebFlux. The uploaded file is received as a FilePart, and the target directory webflux-uploads is created if it doesn’t exist. Using Mono.fromCallable, we determine the target path reactively, and filePart.transferTo(targetPath) streams the file content directly to disk in a non-blocking manner. The thenReturn operator allows returning a confirmation message once the transfer completes.
2.2 Controller: Streaming Multipart Download (WebFlux)
In WebFlux, streaming a multipart response is easiest by using a Flux<DataBuffer> as the response body, with the content type set to multipart/mixed; boundary=.... This method allows you to emit each part of the response, boundaries, headers, and file content, sequentially by reading the files with DataBufferUtils.read without loading everything into memory at once.
@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();
// headers for the part
String header = "--" + BOUNDARY + "\r\n"
+ "Content-Type: application/octet-stream\r\n"
+ "Content-Disposition: attachment; filename=\"" + filename + "\"\r\n\r\n";
DataBuffer headerBuf = bufferFactory.wrap(header.getBytes());
// content Flux<DataBuffer>
Flux<DataBuffer> content = DataBufferUtils.read(
file,
bufferFactory,
4096
);
// after content, produce CRLF
DataBuffer tail = bufferFactory.wrap("\r\n".getBytes());
return Flux.concat(Mono.just(headerBuf), content, Mono.just(tail));
})
// After all parts, produce closing boundary
.concatWith(Mono.just(bufferFactory.wrap(("--" + BOUNDARY + "--\r\n").getBytes())));
} catch (Exception ex) {
partsFlux = Flux.just(bufferFactory.wrap(("Error: " + ex.getMessage()).getBytes()));
}
return response.writeWith(partsFlux)
.doOnError(e -> log.error("Streaming failed", e));
}
}
We compute a Flux<DataBuffer> that, for each file, emits a header buffer, followed by the file content as a Flux<DataBuffer] using DataBufferUtils.read(InputStreamSupplier, factory, bufferSize), and then a trailing CRLF. concatMap preserves the sequential order of the parts. Finally, we append the closing boundary.
Using response.writeWith(partsFlux) writes the DataBuffer stream to the client as it becomes available, allowing the client to process earlier parts while the server continues reading the remaining files.
3. Conclusion
In this article, we explored how to efficiently stream multipart data in Spring using both Spring MVC and Spring WebFlux. We demonstrated sequential streaming for uploads and downloads, showing how Spring MVC handles file transfers using MultipartFile and StreamingResponseBody, and how WebFlux leverages FilePart and Flux<DataBuffer> for non-blocking, reactive streaming. By streaming data directly from the request or to the response, applications can handle large files without exhausting memory, improve performance, and support high-concurrency scenarios.
4. Download the Source Code
This article explored how to handle multipart data streaming in Spring.
You can download the full source code of this example here: spring streaming multipart data




