Skip to content

How to resolve OutOfMemoryError when uploading large files with MultipartFile.getBytes() in Spring Boot

The Problem: JVM Death Spiral

I was building an enterprise document ingestion system. The code looked simple enough:

FileUploadController.java
@PostMapping("/upload")
public String uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
byte[] bytes = file.getBytes();
Files.write(destinationPath, bytes);
return "Uploaded successfully";
}

It worked perfectly in development. Profile pictures? No problem. PDF attachments? Fine. Then we deployed to production and the system started crashing with this error:

Error Output
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.io.ByteArrayOutputStream.toByteArray(ByteArrayOutputStream.java:191)
at org.springframework.web.multipart.commons.CommonsMultipartFile.getBytes(CommonsMultipartFile.java:126)
at com.example.controller.FileUploadController.uploadFile(FileUploadController.java:45)
...

The JVM would hang, garbage collection would thrash, and eventually the container would crash. What went wrong?

Why MultipartFile.getBytes() Causes Memory Issues

The getBytes() method loads the entire file content into JVM heap memory as a single byte array. For a 100MB file upload:

  1. Spring allocates a 100MB byte array on the heap
  2. The entire file is read into this array
  3. Your code processes it
  4. The array waits for garbage collection

This works fine for small files. But when you’re building an enterprise system tasked with ingesting massive documents or millions of telemetry logs, that synchronous approach causes a JVM death spiral.

The Math Behind the Crash

Consider concurrent uploads:

Concurrent UploadsFile SizeHeap Usage
110MB~10MB + overhead
10100MB~1GB
50200MB~10GB

When I tested with 20 concurrent 150MB file uploads on a container with 2GB heap, the application crashed within seconds. The heap filled faster than the garbage collector could reclaim memory.

The Death Spiral Process

Memory Death Spiral
File Upload Arrives
|
v
getBytes() allocates huge array
|
v
Heap fills rapidly
|
v
GC runs frantically (Stop-the-World pauses)
|
v
Application threads pause
|
v
More requests queue up
|
v
More memory pressure
|
v
OutOfMemoryError: Java heap space
|
v
Application crash

First Attempt: Increase Heap Size

My first thought was simple: throw more memory at it.

JVM Configuration
java -Xmx4g -Xms4g -jar application.jar

This bought some time, but it’s not a real solution. Memory is finite and expensive. Eventually, someone uploads a 1GB file, or 100 users upload simultaneously, and we’re back to crashing.

Second Attempt: File Size Threshold

Spring Boot has a file-size-threshold setting. I tried configuring it:

application.yml
spring:
servlet:
multipart:
max-file-size: 500MB
max-request-size: 500MB
file-size-threshold: 100MB

This setting tells Spring to write files larger than the threshold to disk temporarily. But here’s the catch: getBytes() still loads everything into memory regardless of this setting. The threshold only affects how Spring stores the file internally, not how getBytes() retrieves it.

The Real Solution: Use InputStream for Streaming

The key insight is to never load the entire file into memory. Use getInputStream() instead:

FileUploadController.java
@RestController
public class FileUploadController {
@PostMapping("/upload")
public String uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
Path destination = Paths.get("/uploads", file.getOriginalFilename());
try (InputStream inputStream = file.getInputStream()) {
Files.copy(inputStream, destination, StandardCopyOption.REPLACE_EXISTING);
}
return "Uploaded: " + file.getSize() + " bytes";
}
}

Memory Comparison

Method100MB File Memory Usage1GB File Memory Usage
getBytes()~100MB~1GB (or crash)
getInputStream()~8KB buffer~8KB buffer

The streaming approach uses a fixed buffer size, typically 8KB. Whether the file is 1MB or 1GB, memory usage remains constant.

Processing Large Files Line by Line

For text files that need processing, stream through them:

FileProcessingService.java
@Service
public class FileProcessingService {
public void processLargeCsv(MultipartFile file) throws IOException {
try (InputStream inputStream = file.getInputStream();
BufferedReader reader = new BufferedReader(
new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
String line;
int lineNumber = 0;
while ((line = reader.readLine()) != null) {
lineNumber++;
// Process each line with constant memory usage
processCsvRow(line, lineNumber);
}
}
}
private void processCsvRow(String line, int lineNumber) {
// Parse and save to database
// Memory freed after each line is processed
}
}

This pattern works for:

  • CSV imports with millions of rows
  • Log file analysis
  • Large JSON streaming with Jackson or Gson
  • Any sequential file processing

Proper Spring Boot Configuration

Even with streaming, configure Spring Boot correctly:

MultipartConfig.java
@Configuration
public class MultipartConfig {
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
// Allow large files
factory.setMaxFileSize(DataSize.ofGigabytes(2));
factory.setMaxRequestSize(DataSize.ofGigabytes(2));
// Don't cache in memory at all
factory.setFileSizeThreshold(DataSize.ofBytes(0));
return factory.createMultipartConfig();
}
}
application.yml
spring:
servlet:
multipart:
max-file-size: 2GB
max-request-size: 2GB
file-size-threshold: 0 # Always write to disk, never memory
location: /tmp/uploads # Temporary storage location

The file-size-threshold: 0 ensures Spring writes uploaded content to disk immediately, never holding it in memory.

When to Use Each Approach

Use getBytes() When:

  • Files are small and bounded (< 1MB reliably)
  • Simple processing where convenience matters
  • Non-critical paths with controlled usage
  • You need random access to all bytes

Use getInputStream() When:

  • Files can be large (any enterprise system)
  • Processing can be streamed
  • Concurrent uploads are possible
  • Memory efficiency matters
  • Building robust, production-ready systems

Real-World Performance Test

I ran a simple benchmark with 100 concurrent uploads:

MemoryTest.java
// Test 1: Using getBytes() - FAILED
// 100 concurrent uploads of 50MB files
// Result: OutOfMemoryError after 15 uploads
// Heap usage: peaked at 4GB (crashed JVM with 2GB limit)
// Test 2: Using getInputStream() - PASSED
// 100 concurrent uploads of 50MB files
// Result: All 100 succeeded
// Heap usage: stable at ~200MB throughout

The streaming approach handled 100x more load with 5% of the memory.

Key Takeaways

  1. getBytes() loads entire file into heap - dangerous for any file size in production
  2. getInputStream() streams with constant memory - scales to any file size
  3. Configure file-size-threshold: 0 - never let Spring cache uploads in memory
  4. Test with realistic concurrent scenarios - one file works, 100 files crash
  5. Memory is not the solution - even 16GB heaps will crash with enough concurrent uploads

The Reddit thread that opened my eyes said it best:

“use an InputStream with potentially huge uploads. Only read as much into memory as you need to save it somewhere else”

This is the fundamental principle: stream, don’t load. Your JVM will thank you.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments