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:
@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:
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:
- Spring allocates a 100MB byte array on the heap
- The entire file is read into this array
- Your code processes it
- 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 Uploads | File Size | Heap Usage |
|---|---|---|
| 1 | 10MB | ~10MB + overhead |
| 10 | 100MB | ~1GB |
| 50 | 200MB | ~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
File Upload Arrives | vgetBytes() allocates huge array | vHeap fills rapidly | vGC runs frantically (Stop-the-World pauses) | vApplication threads pause | vMore requests queue up | vMore memory pressure | vOutOfMemoryError: Java heap space | vApplication crashFirst Attempt: Increase Heap Size
My first thought was simple: throw more memory at it.
java -Xmx4g -Xms4g -jar application.jarThis 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:
spring: servlet: multipart: max-file-size: 500MB max-request-size: 500MB file-size-threshold: 100MBThis 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:
@RestControllerpublic 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
| Method | 100MB File Memory Usage | 1GB 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:
@Servicepublic 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:
@Configurationpublic 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(); }}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 locationThe 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:
// 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 throughoutThe streaming approach handled 100x more load with 5% of the memory.
Key Takeaways
getBytes()loads entire file into heap - dangerous for any file size in productiongetInputStream()streams with constant memory - scales to any file size- Configure
file-size-threshold: 0- never let Spring cache uploads in memory - Test with realistic concurrent scenarios - one file works, 100 files crash
- 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:
- 👨💻 Reddit Discussion - MultipartFile.getBytes() OOM
- 👨💻 Spring Framework MultipartFile Documentation
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments