Learn to handle large data exports in Laravel without hitting memory limits or timeouts. Discover how to stream CSVs to S3 using queued background jobs.
Previously in this course, we covered Database Connection Pooling to keep our database interactions efficient. This lesson builds on that foundation by addressing the "Export Problem": when a user requests a report containing hundreds of thousands of rows, traditional request-response cycles fail due to PHP memory limits and execution timeouts.
To build a production-grade SaaS, you must treat data exports as asynchronous background tasks.
When you attempt to fetch 50,000 Eloquent models and convert them into a CSV in a single request, you hit two walls:
We solve this by decoupling the export process into a queued job that streams data directly to a cloud storage bucket (like S3).
Instead of returning a file directly to the user, the flow becomes:
GenerateReportJob.League\Csv or Laravel's built-in Storage facade.First, ensure you have league/csv installed. It is the industry standard for memory-efficient CSV generation.
PHPnamespace App\Jobs; use App\Models\User; use League\Csv\Writer; use Illuminate\Support\Facades\Storage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; class GenerateUserReport implements ShouldQueue { use Queueable; public function handle() { $filePath = 'exports/users-' . now()->timestamp . '.csv'; $stream = fopen('php:#6A9955">//temp', 'r+'); $csv = Writer::createFromStream($stream); #6A9955">// Add header $csv->insertOne(['ID', 'Name', 'Email', 'Created At']); #6A9955">// Use chunkById to keep memory usage constant User::query()->chunkById(1000, function ($users) use ($csv) { foreach ($users as $user) { $csv->insertOne([$user->id, $user->name, $user->email, $user->created_at]); } }); #6A9955">// Upload to S3 Storage::disk('s3')->put($filePath, $stream); fclose($stream); #6A9955">// Dispatch event to notify user... } }
By using chunkById(), we ensure that only 1,000 records reside in memory at any given time, regardless of whether we are exporting 1,000 or 1,000,000 rows.
| Method | Memory Usage | Timeout Risk | UX |
|---|---|---|---|
Collection::all() | High (O(n)) | High | Poor (Loading spinner) |
cursor() | Low (O(1)) | Moderate | Better |
| Queued Streaming | Minimal (O(1)) | None | Excellent (Async) |
ExportReport job.chunkById pattern to fetch records from your core domain model.php://temp to construct the CSV.Storage disk.Notification trigger at the end of the handle() method to inform the user via your frontend (e.g., using Laravel Echo).chunkById, if you eager-load relationships inside the loop, you will blow up your memory. Only select the columns you need: User::select(['id', 'name', 'email'])->chunkById(...).DB_READ_TIMEOUT is configured appropriately or perform periodic reconnect() calls if necessary.php://temp or a dedicated temporary directory that is cleared after the job finishes to avoid filling up server storage.Performance in data-heavy SaaS applications relies on avoiding "bloated" requests. By moving exports to the background, you keep your web processes lean and your application responsive. Always stream using chunking to maintain a flat memory profile, and leverage your queue system to handle the heavy lifting.
Up next, we will look at Security Header Configuration to ensure our exported files and browser sessions remain locked down against modern web threats.
Master scalable file uploads in Laravel. Learn to stream directly to S3 and process heavy files asynchronously to keep your application fast and memory-efficient.
Read moreMaster Laravel queue worker prioritization by implementing named queues. Learn to isolate critical tasks from background jobs for a scalable, responsive system.
Handling Large Data Exports