Uploading files larger than 1–5 GB in a normal request often leads to timeout issues, memory spikes, or failed uploads. Modern apps like Google Drive, Dropbox, and WeTransfer solve this using chunk uploads — splitting a file into small pieces, uploading sequentially, then merging on the server.

In this tutorial, we'll implement the same capability in Laravel, with resume support, so that users can continue uploading even after a network interruption.

Not a member on medium.com? Read the full post by Clicking here!

Real-World Scenario

Imagine you're building a Video Collaboration Platform for Production Studios where filmmakers upload raw camera footage. A single file may be 20GB+, and your users might upload from unstable network connections.

You need:

✔ Upload large files without timeout ✔ Progress must be recoverable ✔ Ability to pause/resume ✔ No memory overload on the server ✔ Laravel-friendly implementation

Let's build it!

Backend Strategy (Laravel Perspective)

Instead of thinking:

"How do I upload chunks from JS?"

We start with:

"How should Laravel receive, track, validate, and reconstruct a large file safely?"

The backend will:

  1. Identify an upload session
  2. Accept file chunks independently
  3. Persist upload state
  4. Merge chunks only when complete
  5. Clean up safely

Everything else is secondary.

Step 1: Define Upload Session Contract (Laravel)

Each upload must have a stable identity so it can resume.

Laravel expects:

None

Step 2: Laravel Controller (Chunk-Aware)

php artisan make:controller ChunkedUploadController

Receive & Store Chunks

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

class ChunkedUploadController extends Controller
{
    /**
     * Store a single chunk
     */
    public function store(Request $request)
    {
        $request->validate([
            'upload_id'     => 'required|string',
            'chunk_index'   => 'required|integer',
            'total_chunks'  => 'required|integer',
            'file'          => 'required|file',
            'original_name' => 'required|string',
        ]);

        $uploadId = $request->upload_id;
        $chunkIndex = $request->chunk_index;
        $totalChunks = $request->total_chunks;

        $path = "chunks/{$uploadId}";
        Storage::put(
            "{$path}/{$chunkIndex}.part",
            $request->file('file')->get()
        );

        if ($this->allChunksUploaded($path, $request->total_chunks)) {
            return $this->mergeChunks(
                $path,
                $request->original_name,
                $request->total_chunks
            );
        }

        return response()->json(['status' => 'chunk_stored']);
    }

    /**
     * Check if all chunks are uploaded
     */
    protected function allChunksUploaded(string $path, int $total): bool
    {
        return count(Storage::files($path)) === $total;
    }
}

Step 3: Chunk Merge — Laravel Controls the Final File

/**
 * Merge chunks into final file
 */
protected function mergeChunks(string $path, string $fileName, int $totalChunks)
{
    $finalPath = "uploads/{$fileName}";
    Storage::put($finalPath, '');

    for ($i = 0; $i < $totalChunks; $i++) {
        Storage::append(
            $finalPath,
            Storage::get("{$path}/{$i}.part")
        );
    }

    Storage::deleteDirectory($path);

    return response()->json([
        'status' => 'completed',
        'file' => Storage::url($finalPath),
    ]);
}

Why this works well in Laravel:

  • No file ever lives in memory
  • Merging happens sequentially
  • Storage driver can be swapped (local → S3 → MinIO)

Step 4: Resume Support (Backend-Driven)

Resuming uploads is not magic — it's state awareness.

Laravel exposes existing chunk state:

/**
 * Return already uploaded chunk indexes (resume support)
 */
public function status(Request $request)
{
    $request->validate([
        'upload_id' => 'required|string',
    ]);

    $uploadId = $request->upload_id;
    $path = "chunks/{$uploadId}";

    if (!Storage::exists($path)) {
        return response()->json([]);
    }

    $chunks = collect(Storage::files($path))
        ->map(fn ($file) => (int) basename($file, '.part'))
        ->values();

    return response()->json($chunks);
}

Route:

use App\Http\Controllers\ChunkedUploadController;

Route::prefix('upload')->group(function () {
    Route::post('/chunk',  [ChunkedUploadController::class, 'store']);
    Route::post('/status', [ChunkedUploadController::class, 'status']);
});

Laravel now tells the client:

"These chunks already exist — don't resend them."

Step 5: Minimal Frontend (Contract-Follower)

At this point, JavaScript does not decide anything. It only obeys Laravel.

// Asks Laravel for upload state
async function getUploadedChunks(uploadId) {
    const form = new FormData();
    form.append('upload_id', uploadId);

    const response = await fetch('/upload/status', {
        method: 'POST',
        body: form,
    });

    return await response.json(); // [0,1,2,5,6]
}

// Upload missing chunks
async function uploadFile(file) {
    const chunkSize = 1024 * 1024; // 1MB
    const totalChunks = Math.ceil(file.size / chunkSize);
    const uploadId = `${file.name}-${file.size}`;

    const uploadedChunks = await getUploadedChunks(uploadId);

    for (let index = 0; index < totalChunks; index++) {
        if (uploadedChunks.includes(index)) continue;

        const chunk = file.slice(
            index * chunkSize,
            (index + 1) * chunkSize
        );

        const form = new FormData();
        form.append('file', chunk);
        form.append('upload_id', uploadId);
        form.append('chunk_index', index);
        form.append('total_chunks', totalChunks);
        form.append('original_name', file.name);

        await fetch('/upload/chunk', {
            method: 'POST',
            body: form,
        });
    }
}

Laravel is in charge. JS is disposable.

Final Thoughts

When Laravel owns the upload lifecycle, your application becomes:

  • More reliable
  • Easier to debug
  • Safer under load
  • Future-proof for cloud storage

➣ Follow me on X.com/ilyaskazi ➣ Follow me and subscribe to read such articles on Laravel.