Store and Process CSV Files in the Background Using Laravel Queues

Handling CSV file uploads is a common feature in many web applications, especially dashboards and admin panels. However, processing large CSV files during the request lifecycle can slow down your app. Instead, we can offload the heavy lifting to a background job using Laravel Queues.

In this article, you'll learn how to:

  • Upload and store a CSV file

  • Dispatch a job to process the file asynchronously

  • Save the data into a database

🛠 Prerequisites

  • Laravel 10+

  • Queue configured (e.g. database, redis, sqs)

  • A simple database table to insert records

1️⃣ Set Up the Database

Let’s assume you’re importing users. Create a migration:

                                        
                                            php artisan make:migration create_imported_users_table
                                        
                                    

                                        
                                            // database/migrations/xxxx_xx_xx_create_imported_users_table.php
Schema::create('imported_users', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('email')->unique();
    $table->timestamps();
});
                                        
                                    

Run it:

                                        
                                            php artisan migrate
                                        
                                    

2️⃣ File Upload Form

Create a simple blade file:

<!-- resources/views/upload.blade.php -->
<form action="{{ route('csv.upload') }}" method="POST" enctype="multipart/form-data">
    @csrf
    <input type="file" name="csv" accept=".csv" required>
    <button type="submit">Upload</button>
</form>

3️⃣ Routes and Controller

Create a controller to handle upload:

                                        
                                            php artisan make:controller CsvUploadController
                                        
                                    
                                        
                                            <?php
// app/Http/Controllers/CsvUploadController.php
use Illuminate\Http\Request;
use App\Jobs\ProcessCsv;

class CsvUploadController extends Controller
{
    public function showForm()
    {
        return view('upload');
    }

    public function upload(Request $request)
    {
        $request->validate([
            'csv' => 'required|file|mimes:csv,txt',
        ]);

        $path = $request->file('csv')->store('csv_uploads');

        ProcessCsv::dispatch($path); // Dispatch background job

        return back()->with('message', 'CSV uploaded! Processing in background.');
    }
}
                                        
                                    

Define the routes:

                                        
                                            <?php

// routes/web.php
Route::get('/upload', [CsvUploadController::class, 'showForm']);
Route::post('/upload', [CsvUploadController::class, 'upload'])->name('csv.upload');
                                        
                                    

4️⃣ Create the Job to Process CSV:

                                        
                                            php artisan make:job ProcessCsv
                                        
                                    

                                        
                                            <?php
// app/Jobs/ProcessCsv.php
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
use App\Models\ImportedUser;

class ProcessCsv implements ShouldQueue
{
    use InteractsWithQueue, Queueable, SerializesModels;

    protected $path;

    public function __construct($path)
    {
        $this->path = $path;
    }

    public function handle()
    {
        $file = Storage::get($this->path);
        $lines = explode(PHP_EOL, $file);
        $header = null;

        foreach ($lines as $line) {
            if (empty(trim($line))) continue;
            $data = str_getcsv($line);

            if (!$header) {
                $header = $data;
                continue;
            }

            $userData = array_combine($header, $data);

            // Basic validation can be added here
            ImportedUser::create([
                'name' => $userData['name'] ?? '',
                'email' => $userData['email'] ?? '',
            ]);
        }
    }
}
                                        
                                    

5️⃣ Set Up Queue

If using database driver, configure .env:

                                        
                                            QUEUE_CONNECTION=database
                                        
                                    

Create the queue tables:

                                        
                                            php artisan queue:table
php artisan migrate
                                        
                                    

Then run the queue worker:

                                        
                                            php artisan queue:work
                                        
                                    

✅ Example CSV Format

Make sure your CSV is in this format:

                                        
                                            name,email
Alice,[email protected]
Bob,[email protected]
                                        
                                    

You can follow the next steps to improve it by:

  1. Adding try/catch in the job

  2. Logging errors

  3. Using Laravel’s failed() method on the job class

  4. Adding progress tracking in the database

🔁 Step 6: Add Progress Tracking

Create a new table to track each CSV upload's progress:

                                        
                                            php artisan make:model CsvImport -m
                                        
                                    
                                        
                                            <?php
// database/migrations/xxxx_xx_xx_create_csv_imports_table.php
Schema::create('csv_imports', function (Blueprint $table) {
    $table->id();
    $table->string('file_path');
    $table->enum('status', ['pending', 'processing', 'completed', 'failed'])->default('pending');
    $table->text('error_message')->nullable();
    $table->timestamps();
});
                                        
                                    

Run the migration:

                                        
                                            php artisan migrate
                                        
                                    

Update the upload method to create a tracking record:

                                        
                                            <?php
// CsvUploadController.php
use App\Models\CsvImport;

public function upload(Request $request)
{
    $request->validate([
        'csv' => 'required|file|mimes:csv,txt',
    ]);

    $path = $request->file('csv')->store('csv_uploads');

    $import = CsvImport::create([
        'file_path' => $path,
    ]);

    ProcessCsv::dispatch($import->id);

    return back()->with('message', 'CSV uploaded! Processing in background.');
}
                                        
                                    

🔄 Step 7: Update the Job for Safe Execution

Update ProcessCsv to handle errors and update progress.

                                        
                                            <?php
// app/Jobs/ProcessCsv.php
use App\Models\CsvImport;
use Illuminate\Support\Facades\Log;

class ProcessCsv implements ShouldQueue
{
    use InteractsWithQueue, Queueable, SerializesModels;

    protected $importId;

    public function __construct($importId)
    {
        $this->importId = $importId;
    }

    public function handle()
    {
        $import = CsvImport::findOrFail($this->importId);
        $import->update(['status' => 'processing']);

        try {
            $file = Storage::get($import->file_path);
            $lines = explode(PHP_EOL, $file);
            $header = null;

            foreach ($lines as $line) {
                if (empty(trim($line))) continue;

                $data = str_getcsv($line);
                if (!$header) {
                    $header = $data;
                    continue;
                }

                $userData = array_combine($header, $data);

                ImportedUser::create([
                    'name' => $userData['name'] ?? '',
                    'email' => $userData['email'] ?? '',
                ]);
            }

            $import->update(['status' => 'completed']);
        } catch (\Exception $e) {
            Log::error('CSV Processing Failed: ' . $e->getMessage());

            $import->update([
                'status' => 'failed',
                'error_message' => $e->getMessage(),
            ]);

            throw $e; // This will trigger the failed() method
        }
    }

    public function failed(\Throwable $exception)
    {
        $import = CsvImport::find($this->importId);
        if ($import) {
            $import->update([
                'status' => 'failed',
                'error_message' => $exception->getMessage(),
            ]);
        }

        Log::critical('CSV Job failed: ' . $exception->getMessage());
    }
}
                                        
                                    

🚀 Conclusion

Processing CSV files in the background using Laravel Queues ensures your application stays responsive and scalable. This technique is perfect for bulk imports and large data operations.