Spring Boot Large File Download: Streaming & Range Requests for Resumable Transfers

The article shows how to handle 10GB file downloads in Spring Boot by replacing in-memory byte[] loading with streaming via FileSystemResource and leveraging Spring MVC's built-in Range request support for resumable downloads, while covering security, immutability, and object storage offloading.

LuTiao Programming
LuTiao Programming
LuTiao Programming
Spring Boot Large File Download: Streaming & Range Requests for Resumable Transfers

Problem: In-Memory Byte Array Causes OOM

The initial download endpoint used Files.readAllBytes(path) to load the entire file into a byte[] before returning it via ResponseEntity. This worked for small files (KB to MB) but became catastrophic when files grew to hundreds of megabytes or gigabytes. A 10 GB file would require a 10 GB contiguous heap allocation, which most Spring Boot services cannot sustain.

First Improvement: Streaming with InputStream.transferTo

Switching to a streaming approach avoids loading the whole file into memory:

@GetMapping("/files/{id}/download")
public void download(@PathVariable Long id, HttpServletResponse response) throws IOException {
    FileInfo file = fileService.findById(id);
    Path path = Path.of(file.getPath());
    response.setContentType("application/octet-stream");
    response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getName() + "\"");
    try (InputStream input = Files.newInputStream(path);
         OutputStream output = response.getOutputStream()) {
        input.transferTo(output);
    }
}

Memory stays stable because data flows directly from disk to the socket. However, this still lacks resumable download support: a network interruption forces the client to restart from 0 %.

Resumable Downloads via HTTP Range Requests

HTTP Range requests (RFC 9110) solve the restart problem. The client sends a Range: bytes=104857600- header indicating the byte offset it already has. The server responds with 206 Partial Content, a Content-Range header, and Accept-Ranges: bytes. The client appends the received segment to the existing file.

Spring MVC Native Range Support

Spring MVC automatically parses the Range header and serves partial content when the controller returns a Resource (not an InputStreamResource) or a ResponseEntity with status 200. The framework handles the 206 response and Content-Range calculation.

Correct implementation using FileSystemResource:

@RestController
@RequestMapping("/api/files")
public class FileDownloadController {
    private final FileService fileService;
    public FileDownloadController(FileService fileService) {
        this.fileService = fileService;
    }
    @GetMapping("/{id}/download")
    public ResponseEntity<Resource> download(@PathVariable Long id) throws IOException {
        FileInfo file = fileService.findById(id);
        Path path = Path.of(file.getStoragePath());
        if (!Files.exists(path) || !Files.isRegularFile(path)) {
            throw new FileNotFoundException("file not found");
        }
        Resource resource = new FileSystemResource(path);
        return ResponseEntity.ok()
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .header(HttpHeaders.CONTENT_DISPOSITION,
                        ContentDisposition.attachment()
                                .filename(file.getOriginalName(), StandardCharsets.UTF_8)
                                .build().toString())
                .header(HttpHeaders.ACCEPT_RANGES, "bytes")
                .body(resource);
    }
}

Key points:

No byte[], no Files.readAllBytes(), no manual transferTo. FileSystemResource allows random access required for Range requests; InputStreamResource does not.

Spring sets Accept-Ranges: bytes and processes Range headers automatically.

Testing Range Requests

Full download: curl -v http://localhost:8080/api/files/100/download Partial segment:

curl -v -H "Range: bytes=1048576-2097151" http://localhost:8080/api/files/100/download

Resumable with curl:

curl -C - -O http://localhost:8080/api/files/100/download

Security: Avoid Path Traversal and Missing Authorization

Never accept a file path directly from the client (e.g., GET /download?path=/data/files/a.zip). Instead, use an opaque fileId and resolve the physical path server-side after authorization checks:

public FileInfo findDownloadableFile(Long userId, Long fileId) {
    return fileRepository.findByIdAndUserId(fileId, userId)
            .orElseThrow(FileNotFoundException::new);
}

For multi-tenant systems, include tenantId in the query. This prevents unauthorized access by guessing file IDs.

File Immutability During Resumable Download

If the underlying file is replaced while a download is in progress, the resumed segments will belong to a different version, corrupting the result. Use immutable storage keys: each upload creates a new storage_key (e.g., with a UUID or content hash) rather than overwriting the same path. Combine with ETag or Last-Modified and the If-Range header so the client can verify the resource hasn't changed before resuming.

Memory Model Comparison

Byte-array approach: Disk → huge byte[] in JVM heap → socket. Heap grows linearly with file size.

Streaming approach: Disk → small read buffers → socket. Heap remains constant regardless of file size.

Eliminate any code that materializes the entire file in memory: Files.readAllBytes, resource.getContentAsByteArray(), ByteArrayOutputStream, or byte[] fileContent.

Zero-Copy Optimization with FileChannel.transferTo

For extreme throughput or custom range handling, JDK's FileChannel.transferTo can transfer directly from file to socket channel, potentially leveraging OS zero-copy. Example:

try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.READ);
     WritableByteChannel output = Channels.newChannel(response.getOutputStream())) {
    long position = start;
    long remaining = length;
    while (remaining > 0) {
        long transferred = fileChannel.transferTo(position, remaining, output);
        if (transferred <= 0) break;
        position += transferred;
        remaining -= transferred;
    }
}

However, prefer Spring's built-in Range handling first; only drop to this level if benchmarks show insufficient throughput, high CPU, or special throttling/storage protocols.

Offloading to Object Storage (S3, OSS, COS, MinIO)

For very large files (hundreds of GB) or high concurrency, routing the file through the Java application wastes bandwidth and CPU. Better architecture:

Client requests download.

Spring Boot performs authorization.

Generate a short-lived signed URL (e.g., 5 minutes) for the object storage.

Client downloads directly from object storage/CDN.

This keeps the Java service lightweight and shifts data transfer to infrastructure optimized for it.

Rate Limiting and Concurrency Controls

Even with streaming, unlimited concurrent downloads can saturate disk I/O, network bandwidth, connection pools, and threads. Apply limits at the gateway, CDN, or object storage layer:

Per-user concurrent downloads (e.g., 2 for free tier, 10 for enterprise).

Per-tenant concurrency caps.

Bandwidth throttling.

Download audit logging.

Conclusion

The core issue is not JVM tuning ( -Xmx, Tomcat swallow size, Nginx timeouts) but adapting the code model to the data scale. The final flow for local files:

Client → fileId → Auth → Metadata lookup → FileSystemResource → Spring MVC Range handling → 206 Partial Content → Client resumable download

For object storage:

Client → Spring Boot auth → Signed URL → S3/OSS/COS/MinIO → Client direct download

Both approaches ensure a 10 GB file never becomes a 10 GB byte[] in the JVM heap, and both support resumable downloads so a 9 GB partial transfer is not lost on network interruption.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Memory OptimizationStreamingSpring BootSecurityFile DownloadRate LimitingObject StorageRange RequestsResumable DownloadFileSystemResource
LuTiao Programming
Written by

LuTiao Programming

LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.