How to Handle Multipart File Uploads in Spring Boot REST APIs

How to Handle Multipart File Uploads in Spring Boot REST APIs

QA uploaded a 2MB PDF, and the endpoint that had handled every test file all sprint fell over with a 500. Nothing had changed in the code. The 1MB default had been sitting there since the project’s first commit, waiting for a file just slightly too big.

Multipart file upload in Spring Boot REST API projects starts with a simple 20-minute tutorial and can end with a 1MB default nobody remembers configuring. Spring’s multipart support works well, but only within limits you need to know about.

Multipart file upload in a Spring Boot REST API runs through MultipartFile, with spring.servlet.multipart properties controlling max-file-size, max-request-size, and the temp location. The common pitfalls are the 1MB defaults, whole-file buffering with getBytes(), and unhandled MaxUploadSizeExceededException. For large or unreliable transfers, chunked client-side uploads or a managed service can avoid these servlet limits entirely.

This article walks through the working endpoint, the config properties that actually matter, where memory gets wasted, and how to fail in a way that gives the client a useful response.

Key Takeaways

  • Spring Boot caps uploads at 1MB per file and 10MB per request by default. Both need to be raised deliberately for production.
  • MultipartFile.getBytes() loads the whole file into heap. transferTo() and getInputStream() stream instead, and should be the default choice.
  • MaxUploadSizeExceededException needs its own @ExceptionHandler, or clients get a bare 500 with no useful detail.
  • The multipart temp directory has to exist and be writable at runtime. Containerised deployments break here more often than local ones.
  • Reverse proxies in front of Spring enforce their own body caps. The smallest limit in the whole chain wins, not the one set in application.yml.

The Happy Path, MultipartFile in 20 Lines

A basic Spring Boot upload endpoint is pretty short. In a Spring application, the usual approach is to accept a MultipartFile, validate it, save it somewhere reliable, and return a response.

@RestController

@RequestMapping("/api/files")

public class FileUploadController {

private final Path uploadDir = Paths.get("/data/uploads");

@PostMapping("/upload")

public ResponseEntity<Map<String, String>> uploadFile(

@RequestParam("file") MultipartFile file) throws IOException {

if (file.isEmpty()) {

return ResponseEntity.badRequest().body(Map.of("error", "File is empty"));

}

String filename = StringUtils.cleanPath(file.getOriginalFilename());

Path target = uploadDir.resolve(filename);

file.transferTo(target);

return ResponseEntity.ok(Map.of("filename", filename, "size", String.valueOf(file.getSize())));

}

}

This works, and transferTo does more behind the scenes than it may seem. It writes the file to disk without first loading the whole file into memory. That might not matter much for a small file, but it becomes important as file sizes grow.

The endpoint above will handle small PDFs just fine, but larger, real-world files can expose the limits it’s relying on. That’s where the upload configuration becomes important.

Config That Bites: Sizes, Temp Dirs, Proxies

Spring Boot’s multipart defaults are conservative on purpose, but they may not be enough for a production app. The three main properties to know are under spring.servlet.multipart, and each one should be set based on your needs.

max-file-size sets the limit for a single file. max-request-size sets the limit for the entire request, which matters when a form contains multiple files or extra fields. location sets the temporary directory Spring uses while handling the upload.

That last setting can cause a common problem. The directory must exist and be writable when the app runs. A container might work perfectly in local testing but fail on the first real upload if that directory wasn’t created or doesn’t have the right permissions.

spring:

servlet:

multipart:

enabled: true

max-file-size: 50MB       # raise past the 1MB default deliberately

max-request-size: 55MB    # covers the file plus any form fields

file-size-threshold: 2MB  # below this, Spring keeps the part in memory

location: /tmp/spring-uploads  # must exist and be writable in every environment

These properties only control what Spring allows. This is where many REST API file upload guides stop, but it’s not the whole picture.

A reverse proxy in front of Spring, such as nginx, can have its own request size limit. It doesn’t know or care what you set in application.yml. The smallest limit anywhere in the upload chain wins.

Make sure the proxy’s client_max_body_size is set along with Spring’s multipart limits. Otherwise, the 500 error from earlier may simply turn into a 413 from another layer.

Filestack discord

Getting the configuration right prevents rejected uploads. But it doesn’t tell you what happens to a file after it gets through. That’s where memory problems can start.

Memory Pitfalls and Streaming

MultipartFile gives you two main ways to access a file, and the difference becomes important as file sizes and concurrent uploads grow.

getBytes() loads the entire file into a byte array in memory. For a 5 MB file, that’s usually fine. But a 200 MB file, or twenty 20 MB uploads at the same time, can quickly use a lot of heap and may lead to an OutOfMemoryError. transferTo() and getInputStream() are better choices when you want to avoid loading the whole file into memory at once.

This connects to how multipart uploads work in web applications. The server already receives the request body in chunks. Using getBytes() defeats that benefit by loading the complete file into memory. transferTo() lets the data move directly to disk or another storage destination instead.

Think about your actual traffic, not just one upload. If twenty users upload 20 MB files at the same time, getBytes() could require around 400 MB of heap just for the file contents, before counting anything else the JVM needs. Streaming keeps memory usage much more predictable.

Streaming helps with memory, but it doesn’t solve every upload problem. Files that fail because they’re too large or the connection drops need their own handling, which many tutorials don’t cover.

Resilience, Errors and Resume

An unhandled MaxUploadSizeExceededException can reach the client as a generic 500 error, without explaining that the file was too large.

Users should get a clear response instead. The good news is that you can fix this with a small exception handler.

@ControllerAdvice

public class UploadExceptionHandler {

@ExceptionHandler(MaxUploadSizeExceededException.class)

public ResponseEntity<Map<String, String>> handleMaxSizeException(

MaxUploadSizeExceededException ex) {

return ResponseEntity

.status(HttpStatus.PAYLOAD_TOO_LARGE)

.body(Map.of(

"error", "File exceeds the maximum allowed size",

"maxSize", "50MB"

));

}

}

This turns a confusing 500 into a 413 response with a message the client can actually understand and act on. It’s a small fix, but it can make the difference between a client that handles the error correctly and one that assumes your API is broken.

Handling the error doesn’t solve the bigger problem with very large files, though. Servlet-based multipart uploads are designed mainly for form submissions, not multi-gigabyte transfers. This raises another common question: what’s the best way to handle multipart or resumable uploads for large files?

Past a certain file size, it’s better to move away from a single multipart request. Split the file into smaller chunks on the client, upload those chunks separately, and then reassemble them on the server. Or let a service built for large uploads handle that part for you.

That size limit is the point where you should stop pushing the servlet approach further and consider a different upload strategy.

The Managed Route, Spring on the Control Plane

Past the servlet’s comfort zone, a more reliable approach is to use a managed file upload API and keep Spring focused on validation, storage records, and business logic.

Instead of constantly tuning max-file-size, worrying about memory from concurrent getBytes() calls, or building chunked uploads yourself, the file can go directly from the client to storage. Spring only handles the file metadata after the upload is complete.

Diagram showing config and pitfalls in multipart file upload in spring boot rest api services.

Filestack’s chunked uploads can keep large file transfers out of the servlet layer, supporting files up to 5 GB while Spring handles metadata instead of the file bytes.

If you’re comparing this with building the upload system yourself, two useful questions are: what’s the best file upload API for enterprise applications, and which service offers reliable uptime and upload performance? For those questions, check each provider’s current uptime and status history rather than relying only on marketing claims.

For more on the transport layer, our guide to file upload APIs covers authentication, content types, upload limits, and other common upload patterns. The Filestack API docs also cover the chunked upload flow in more detail.

Now that we’ve covered both approaches, here’s a quick summary of what to do.

Conclusion: Configure on Purpose, Stream by Default

Set your multipart limits deliberately instead of discovering them in production. Avoid calling getBytes() for files that could be large or uploaded at the same time. Use transferTo() or getInputStream() when you want to avoid loading the whole file into memory.

Handle MaxUploadSizeExceededException explicitly too, so clients get a clear 413 response instead of a generic 500.

Once files get too large for a single servlet request, move the upload to a chunked or managed solution instead of pushing the servlet beyond what it was designed to handle.

One quick check: search your config for multipart. If you haven’t set the relevant properties, don’t assume your app has the limits you want. Verify the actual defaults for your Spring Boot version and set them explicitly.

Frequently Asked Questions

Why does Spring Boot reject uploads over 1MB?

That’s the framework default. Set spring.servlet.multipart.max-file-size and max-request-size explicitly to raise it.

Does MultipartFile load files into memory?

getBytes() does, in full. transferTo() and getInputStream() stream the file instead, and should be the default for anything beyond a small file.

How do I handle files larger than servlet limits?

Use chunked client-side uploads that split the file into parts, or hand the transfer to a managed pipeline that carries files up to 5GB without touching the servlet’s limits at all.

Read More →