Back to Scenarios & Design Questions
Q45Question 45 of 67

Why does a Spring Boot MultipartFile upload fail past a few MB, what are the default limits, how do you raise them, and why is raising the limit the wrong fix for truly large uploads

Key Concepts
  • Default limits are ~1 MB per file (max-file-size) and ~10 MB per request (max-request-size).
  • Exceeding them throws MaxUploadSizeExceededException wrapping SizeLimitExceededException.
  • Raise with properties in application.yml or programmatically via MultipartConfigElement from MultipartConfigFactory.
  • Raising the limit is the wrong fix for large files because getBytes() loads the entire file into heap.
  • Stream the InputStream instead: call getInputStream() and copy in fixed-size chunks to keep memory bounded.
  • For truly large files (GB+), bypass the server entirely with presigned direct-to-S3 uploads.
  • Match the mechanism to the size: raise limits for small files, stream for medium, presigned for large.

Definition

Defaults are ~1 MB per file and ~10 MB per request; raise them with properties for modestly larger files, but for truly large files stream the InputStream or go direct-to-storage, because getBytes() buffers the whole file in the heap.
  • Default limits – spring.servlet.multipart.max-file-size is ~1 MB per file; max-request-size is ~10 MB for the entire multipart request.
  • Exception – Exceeding either throws MaxUploadSizeExceededException wrapping the container's SizeLimitExceededException.
  • Raise with properties – Set the values in application.yml or programmatically via MultipartConfigFactory.
  • Wrong fix – Raising the limit on a controller that calls getBytes() converts a clean 413 rejection into a catastrophic OutOfMemoryError.
  • Right fix – Stream with getInputStream() for medium files; use presigned URLs for large files.

National Bank of India's online form has a small letterbox slot big enough for a one-page form (1 MB). When Rajesh tries to post a thick 50 MB loan file, it gets rejected. You can widen the slot (raise limits), but if you make it huge and a thousand customers shove thick files through at once, the mailroom floor (heap) is buried and the whole branch collapses. The real fix is a conveyor belt that carries pages straight to the warehouse as they arrive (streaming), or a gate pass for direct warehouse delivery (presigned URL).

Understanding the Concepts

Why does a Spring Boot MultipartFile upload fail past a few MB?

Spring Boot's autoconfiguration sets guardrails so a huge or malicious upload cannot exhaust server resources. Exceeding either limit throws MaxUploadSizeExceededException.

JAVA
package com.bank.upload.controller;

import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MaxUploadSizeExceededException;

public class UploadController {

    // When Rajesh uploads a 50 MB file with default limits:
    // Tomcat rejects it before the controller even sees it.
    // Exception: MaxUploadSizeExceededException
    //   caused by: SizeLimitExceededException
    //   "the request was rejected because its size (50000000)
    //    exceeds the configured maximum (1048576)"

    public String handleUpload(MultipartFile file) {
        // Never reached for files > 1 MB with defaults
        return "Uploaded: " + file.getOriginalFilename();
    }
}

What are the default limits?

| Property | Default | Meaning |
|---|---|---|
| spring.servlet.multipart.max-file-size | ~1 MB | Maximum size of any single uploaded file |
| spring.servlet.multipart.max-request-size | ~10 MB | Maximum size of the entire multipart request |
| spring.servlet.multipart.file-size-threshold | 0 | Size above which parts spool to disk instead of heap |

How do you raise them with properties?

In application.yml, set the values to allow larger files.

YAML
# application.yml
spring:
  servlet:
    multipart:
      max-file-size: 50MB
      max-request-size: 60MB
      file-size-threshold: 256KB  # spill to disk above this

How do you raise them programmatically?

Define a MultipartConfigElement bean from MultipartConfigFactory.

JAVA
package com.bank.upload.config;

import jakarta.servlet.MultipartConfigElement;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.unit.DataSize;

@Configuration
public class MultipartConfig {

    @Bean
    public MultipartConfigElement multipartConfigElement() {
        MultipartConfigFactory factory = new MultipartConfigFactory();
        factory.setMaxFileSize(DataSize.ofMegabytes(50));
        factory.setMaxRequestSize(DataSize.ofMegabytes(60));
        factory.setFileSizeThreshold(DataSize.ofKilobytes(256));
        return factory.createMultipartConfig();
    }
}

Why is raising the limit the wrong fix for truly large files?

The real danger is how the file is consumed. If your controller calls getBytes(), it loads the entire file into the heap as a byte array. Raising the limit just converts a clean 413 rejection into a catastrophic OOM crash.

JAVA
package com.bank.upload.antipattern;

import org.springframework.web.multipart.MultipartFile;

public class RaiseLimitWrong {

    // WRONG: raised limit to 5 GB, but still calls getBytes()
    public void handleUpload(MultipartFile file) throws Exception {
        byte[] data = file.getBytes(); // 5 GB in heap for ONE request
        // A handful of concurrent uploads = instant OutOfMemoryError
        // The JVM crashes, taking down ALL other users' requests.
    }
}

How do you stream the InputStream correctly?

Instead of getBytes(), call getInputStream() and copy in fixed-size chunks. Only one chunk is in memory at a time.

JAVA
package com.bank.upload.service;

import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;

public class StreamingUploadService {

    // CORRECT: stream in chunks, memory stays bounded
    public void handleUpload(MultipartFile file, Path destination) throws Exception {
        try (InputStream in = file.getInputStream();
             OutputStream out = Files.newOutputStream(destination)) {
            byte[] buffer = new byte[8192]; // 8 KB at a time
            int bytesRead;
            while ((bytesRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
            }
        }
        // Only 8 KB in memory at any time, regardless of file size
    }

    // Or pipe directly to S3:
    // s3Client.putObject(putRequest,
    //     RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
}

When should you use which mechanism?

Match the mechanism to the file size.

| File Size | Mechanism | Why |
|---|---|---|
| Small (< 10 MB) | Default MultipartFile + raised limits | Simple, getBytes() is safe |
| Medium (10-500 MB) | MultipartFile + stream InputStream | Bounded memory |
| Large (500 MB+) | Presigned URL (direct-to-S3) | Server never touches bytes |
| Very Large (multi-GB) | S3 Multipart Upload | Resumability, parallelism |

JAVA
package com.bank.upload.service;

public class UploadStrategySelector {

    public String selectStrategy(long sizeBytes) {
        if (sizeBytes < 10_000_000) {
            return "MultipartFile with getBytes() (simple, safe for small files)";
        } else if (sizeBytes < 500_000_000) {
            return "MultipartFile with getInputStream() (stream, bounded memory)";
        } else {
            return "Presigned URL direct-to-S3 (server never touches bytes)";
        }
    }
}

How do you handle the exception gracefully?

Catch MaxUploadSizeExceededException in a @ControllerAdvice and return a clean 413.

JAVA
package com.bank.upload.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.multipart.MaxUploadSizeExceededException;

@ControllerAdvice
public class UploadExceptionHandler {

    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public ResponseEntity<String> handleMaxUploadSize(
            MaxUploadSizeExceededException ex) {
        return ResponseEntity
            .status(HttpStatus.PAYLOAD_TOO_LARGE) // 413
            .body("File too large. Maximum allowed size is 50 MB.");
    }
}

What are common interview follow-up questions?

What is the difference between file-size-threshold and max-file-size? How does Tomcat handle multipart parsing internally? What happens if the temp directory fills up during spooling? How would you implement progress tracking for a large upload? What is the difference between StandardServletMultipartResolver and CommonsMultipartResolver?

Visual Overview

DIAGRAM
┌──────────────────────────────────────────────────────────┐
│         SPRING BOOT MULTIPART UPLOAD DECISION TREE        │
├──────────────────────────────────────────────────────────┤
│                                                          │
│  File size?                                              │
│     │                                                    │
│     ├── < 1 MB ──▶ Default limits OK, getBytes() safe    │
│     │                                                    │
│     ├── 1-50 MB ──▶ Raise limits in application.yml      │
│     │                stream getInputStream() to dest     │
│     │                                                    │
│     ├── 50-500 MB ──▶ Raise limits + stream              │
│     │                  consider WebFlux for concurrency   │
│     │                                                    │
│     └── 500 MB+ ──▶ Presigned URL (direct to S3)         │
│                      S3 multipart for multi-GB            │
│                      Server never touches bytes           │
│                                                          │
│  WRONG: raise to 5 GB + getBytes()                       │
│    → 5 GB in heap per request → OutOfMemoryError         │
│                                                          │
│  CORRECT: match mechanism to size                        │
│    → small: getBytes() │ medium: stream │ large: presign │
└──────────────────────────────────────────────────────────┘

Key Takeaway

Spring Boot's default multipart limits exist to protect the server: max-file-size is about 1 MB per file and max-request-size is about 10 MB for the whole request. When Rajesh tried uploading a 50 MB loan document at National Bank of India, it was rejected with MaxUploadSizeExceededException. The first fix is to catch that exception in a ControllerAdvice and return a clean 413 so the client understands.

To allow larger files, Pankaj raised the limits in application.yml and also set a file-size-threshold so parts above that size spill to disk instead of sitting in the heap. But raising the limit is the wrong answer for truly large files. The real constraint is memory, not the configured number. If the controller calls getBytes(), it loads the entire file as a byte array. After the team bumped the limit to 5 GB, a few concurrent 40 MB uploads buried the heap because the code still called getBytes(), converting a clean rejection into a catastrophic OOM crash.

The correct approach is to stream: call getInputStream() and copy in fixed-size chunks, or pipe directly to S3 with RequestBody.fromInputStream(), so only one chunk is in memory at a time. For truly large files (gigabytes), bypass the server entirely with presigned URLs or S3 multipart upload. The principle is to match the mechanism to the size: raise limits and getBytes() for small files, stream for medium, and presigned for large.

Remember one sentence: The default limits are ~1 MB per file and ~10 MB per request; raise them for modest files but stream the InputStream for large ones and go direct-to-S3 for truly large files, because raising limits on a getBytes() controller converts a clean 413 into an OutOfMemoryError.

---

Share
Use ← → arrow keys to navigate