The most common mistake developers make when exporting thousands of database records is loading every row into memory before generating the Excel file. Calling Model::all() or Model::get() inside an export may work perfectly during development, then fail when the production database grows.
If you need to export large data with chunk in laravel-excel, the fix is usually to stop building a full collection and let Laravel Excel retrieve database records in smaller batches.
Quick answer: Use Laravel Excel's FromQuery concern instead of FromCollection with Model::all() or Model::get(). Return an Eloquent query without executing it. Laravel Excel processes the query in chunks automatically. For larger exports, queue the operation so it runs outside the HTTP request and avoids browser timeouts.
This guide uses one example throughout: exporting customer orders to an Excel spreadsheet. We'll start with the memory-heavy approach, replace it with a chunked query, and then move the export into Laravel's queue system.
The implementation follows the Laravel Excel 3.1 API. If you're using another major version, check the corresponding documentation before copying the code.
1. Reproduce the memory problem with a large Laravel export
Imagine an order management application containing a growing number of customer orders.
The orders table has these columns:
| Column | Purpose |
|---|---|
id |
Order identifier |
order_number |
Reference number |
customer_email |
Customer email |
total |
Order amount |
created_at |
Order creation date |
Your application already has an App\Models\Order Eloquent model.
A typical export begins with this command:
php artisan make:export OrdersExport --model=Order
If Artisan reports that the command does not exist, install Laravel Excel first:
composer require maatwebsite/excel
This installs the package version compatible with your project's Composer dependencies. Check which version was selected:
composer show maatwebsite/excel
For an existing installation, you don't need to reinstall the package.
The first implementation developers often write looks like this:
<?php
namespace App\Exports;
use App\Models\Order;
use Maatwebsite\Excel\Concerns\FromCollection;
class OrdersExport implements FromCollection
{
public function collection()
{
return Order::all();
}
}
And the controller might return:
return Excel::download(
new OrdersExport(),
'orders.xlsx'
);
This works for small datasets because the entire result fits comfortably in memory.
The problem is here:
return Order::all();
Eloquent loads every matching record into a collection before Laravel Excel begins processing it.
Every record becomes an Eloquent model containing attributes and additional object overhead. On top of that, PhpSpreadsheet needs resources to construct the workbook.
For a sufficiently large dataset, PHP may fail with:
Allowed memory size of 134217728 bytes exhausted
The exact memory limit and error location depend on your PHP configuration and the amount of data.
Another possible symptom is an HTTP timeout while the browser waits for the export.
Increasing memory_limit might temporarily hide the problem, but it doesn't remove the underlying inefficient data-loading approach.
Expected result at this stage: You have identified whether your export retrieves the entire dataset before writing the spreadsheet.
2. Export large data with chunk in laravel-excel using FromQuery
The recommended solution for database-backed exports is Laravel Excel's FromQuery concern.
Unlike FromCollection, it accepts an unexecuted Eloquent query. Laravel Excel then retrieves the results in chunks rather than requiring you to load all matching records at once.
Replace app/Exports/OrdersExport.php with:
<?php
namespace App\Exports;
use App\Models\Order;
use Maatwebsite\Excel\Concerns\FromQuery;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
class OrdersExport implements
FromQuery,
WithHeadings,
WithMapping
{
public function query()
{
return Order::query()
->select([
'id',
'order_number',
'customer_email',
'total',
'created_at',
])
->orderBy('id');
}
public function headings(): array
{
return [
'ID',
'Order Number',
'Customer Email',
'Total',
'Created At',
];
}
public function map($order): array
{
return [
$order->id,
$order->order_number,
$order->customer_email,
$order->total,
$order->created_at?->format('Y-m-d H:i:s'),
];
}
}
The crucial detail is that query() returns a builder.
Don't do this:
public function query()
{
return Order::query()->get();
}
And don't do this:
public function query()
{
return Order::all();
}
Both execute the database query and return the records immediately.
With FromQuery, Laravel Excel controls when and how the query runs.
Notice three other decisions in the working example.
First, select() retrieves only the fields required by the spreadsheet. This avoids loading unnecessary model attributes.
Second, orderBy('id') provides a deterministic ordering for the export. This is useful when processing results across multiple queries, although concurrent database modifications can still affect exports.
Third, WithMapping explicitly controls the spreadsheet columns. You don't need to export every model property.
According to the official Laravel Excel FromQuery documentation, chunking happens automatically when using this concern.
Expected result: The export now processes order records in database batches rather than loading the entire result into one collection.
One distinction matters: database chunking reduces the memory required for retrieving records, but it doesn't guarantee constant total memory usage. Generating a large XLSX workbook can still consume substantial memory.
3. Control the export chunk size without manual loops
Sometimes the default export chunk size isn't suitable for your workload.
Laravel Excel provides the WithCustomChunkSize concern for controlling how many query records are processed per chunk.
Modify the export class declaration:
use Maatwebsite\Excel\Concerns\WithCustomChunkSize;
class OrdersExport implements
FromQuery,
WithHeadings,
WithMapping,
WithCustomChunkSize
{
// Keep the query(), headings(), and map()
// methods from the previous step.
public function chunkSize(): int
{
return 1000;
}
}
Add the use statement and chunkSize() method to the existing export class; don't remove the implementations from Step 2.
The 1000 value is an example configuration, not a universally optimal setting.
Smaller chunks reduce the number of database records handled simultaneously but may require more queries and queued jobs.
Larger chunks reduce query overhead but can increase memory consumption during processing.
Choose a value based on actual measurements, especially if individual rows contain large text fields or require additional database relationships.
Why not call chunk() manually?
You might find examples like this:
Order::chunk(1000, function ($orders) {
// Export each batch...
});
Laravel's chunk() method is useful when processing records yourself. However, manually calling it inside a normal Laravel Excel export doesn't automatically create one continuous spreadsheet.
You would need to coordinate writing and appending the batches.
With FromQuery, Laravel Excel already manages the underlying query chunks.
Also, don't confuse WithCustomChunkSize with WithChunkReading.
WithChunkReading belongs to Laravel Excel's import workflow. It is commonly used when reading large Excel files into Laravel, not when exporting database records.
See the Laravel Excel export concerns reference for the supported interfaces.
Expected result: Your export retrieves records in a controlled chunk size without custom database loops.
4. Queue the Laravel Excel export to prevent request timeouts
Chunking solves one part of the problem: retrieving too much database data at once.
However, an export may still take longer than the PHP request timeout or web server timeout.
This is why a chunked export can work through Artisan but fail when triggered from the browser.
For large reports, queueing is generally a better approach than forcing the user to wait for a synchronous download.
Laravel Excel supports queued exports directly.
First, configure a queue connection in .env:
QUEUE_CONNECTION=database
Make sure your application's queue tables exist. Newer Laravel application templates may already include their migrations.
If your project doesn't have a jobs-table migration, generate one using the appropriate command for your Laravel version:
php artisan make:queue-table
Older Laravel installations may use:
php artisan queue:table
Run migrations after creating any missing table:
php artisan migrate
Then start a queue worker:
php artisan queue:work database --tries=3 --timeout=300
The timeout shown is an example. Configure the queue connection's retry_after value to exceed the worker timeout, and use worker settings appropriate for your workload.
Now create a controller:
php artisan make:controller OrderExportController
In app/Http/Controllers/OrderExportController.php:
<?php
namespace App\Http\Controllers;
use App\Exports\OrdersExport;
use Illuminate\Http\RedirectResponse;
class OrderExportController extends Controller
{
public function export(): RedirectResponse
{
(new OrdersExport())->queue(
'exports/orders.xlsx',
'local'
);
return back()->with(
'success',
'Your export has been queued.'
);
}
}
The queue() method is available through Laravel Excel's Exportable trait, so add it to the export class:
use Maatwebsite\Excel\Concerns\Exportable;
Inside the class:
use Exportable;
For clarity, the export class now implements FromQuery, WithHeadings, WithMapping, and WithCustomChunkSize, uses the Exportable trait, and retains all four methods from the earlier steps.
Register the route in routes/web.php:
use App\Http\Controllers\OrderExportController;
use Illuminate\Support\Facades\Route;
Route::middleware('auth')->post(
'/orders/export',
[OrderExportController::class, 'export']
)->name('orders.export');
The route is protected by Laravel's authentication middleware, although you should also add any application-specific export authorization.
To trigger it from an authenticated Blade page:
<form method="POST" action="{{ route('orders.export') }}">
@csrf
<button type="submit">
Export Orders
</button>
</form>
When a user clicks the button, Laravel dispatches the export to the queue rather than waiting for the entire workbook to be generated.
The background worker processes the export chunks and saves the resulting file to the configured disk.
Important: Queuing returns immediately, but it does not immediately provide a completed downloadable file.
Your application should show an export status and only offer a download after completion.
This example uses a fixed filename for simplicity. In production, generate a unique filename per export request to prevent users from overwriting one another's reports. Downloads must also be authorized against the requesting user.
5. Fix Laravel Excel out-of-memory and failed queue jobs
If the queued export still fails, don't immediately increase PHP memory and assume the problem is resolved.
Check the failure category first.
The worker never processes the export
If the controller reports success but no file appears, verify that the queue worker is running.
php artisan queue:work database --tries=3 --timeout=300
Inspect failed jobs:
php artisan queue:failed
To inspect the application log, check:
storage/logs/laravel.log
If you corrected the underlying error, retry an appropriate failed job using its recorded identifier:
php artisan queue:retry FAILED_JOB_ID
Replace FAILED_JOB_ID with an actual identifier reported by Artisan.
Queued jobs don't complete merely because they were dispatched. A worker must actively consume them.
Memory is still exhausted
If FromQuery is already implemented, check the size and complexity of each spreadsheet row.
Avoid unnecessarily loading relationships or calculating expensive values inside map().
For example, this can introduce repeated database queries:
public function map($order): array
{
return [
$order->id,
$order->customer?->name,
];
}
If the related customer isn't loaded, accessing it may trigger additional queries.
Consider an appropriately scoped eager load or a database join. Select the approach based on the actual relationships and the size of the export.
If the spreadsheet contains extensive styling, formulas, images, or very wide rows, PhpSpreadsheet's workbook memory requirements may become the limiting factor even though database queries are chunked.
For tabular reports that don't require XLSX features, CSV may be a better format.
Queue jobs run but the output is incomplete
Queued exports rely on multiple jobs processing the same workbook.
On a multi-server deployment, check whether workers have access to shared temporary files. Laravel Excel supports configuring a remote temporary-files disk for this situation.
For complex queries containing groupBy, the standard query count may not reflect the number of result rows. In that situation, Laravel Excel's WithCustomQuerySize concern can supply the correct row count for queued job calculation.
Also check for records being inserted, deleted, or modified during a long-running export. Ordinary chunked queries don't automatically provide a consistent database snapshot.
A MySQL data error appears during report preparation
Exporting rows normally reads data rather than inserting it. But some systems first populate staging or reporting tables before generating spreadsheets.
If that preparatory process produces a Data truncated for column error, fix the database value or column definition separately. It isn't solved by changing the Excel chunk size.
See Data Truncated for Column at Row 1: Cause and Fix for that related database issue.
Expected result: You can distinguish memory exhaustion, request timeouts, failed workers, missing temporary files, and source-data errors instead of treating them as the same problem.
6. Verify that the chunked export contains every order
A successful queue response doesn't prove that the final spreadsheet is complete.
Verification should include the stored file, the number of exported rows, and a check of sample records.
For the controller example, wait until the queue worker finishes, then run:
php artisan tinker
Inside Tinker:
use Illuminate\Support\Facades\Storage;
Storage::disk('local')->exists('exports/orders.xlsx');
Expected output after a successful export:
true
Check the file size:
Storage::disk('local')->size('exports/orders.xlsx');
A positive size proves that a file exists, but not that every expected record was written.
For a smaller verification dataset, open the spreadsheet and compare its exported data rows with the database query:
\App\Models\Order::query()->count();
Since the example contains one heading row, the spreadsheet should have one more row than the number of exported orders, provided that each order maps to exactly one row.
You can also test that the controller dispatches a queued export without generating a real spreadsheet. Laravel Excel provides a facade fake for this purpose.
Example feature test:
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Maatwebsite\Excel\Facades\Excel;
use Tests\TestCase;
class OrderExportTest extends TestCase
{
use RefreshDatabase;
public function test_orders_export_is_queued(): void
{
Excel::fake();
$user = User::factory()->create();
$this->actingAs($user)
->post(route('orders.export'))
->assertRedirect();
Excel::assertQueued(
'exports/orders.xlsx',
'local'
);
}
}
Run it using:
php artisan test --filter=OrderExportTest
This verifies that the application queues the intended export. It does not prove that the actual workbook is complete or that the queue worker can process it.
For that, perform an integration test with a real worker and compare the file contents against a controlled dataset.
Next steps
Once the export works reliably, add unique filenames, export progress tracking, completion notifications, and secure download links. Measure query duration, peak memory, worker runtime, and final workbook size before tuning chunk settings. For particularly large reports, consider streaming or background-generated CSV files instead of creating a heavily formatted XLSX workbook.
FAQ
Does Laravel Excel automatically export data in chunks?
Yes, when you use the FromQuery concern, Laravel Excel processes the returned database query in chunks. You don't need to manually call Eloquent's chunk() method. This differs from exporting an already-loaded collection, which may consume significant memory before spreadsheet generation begins.
What is the best chunk size for Laravel Excel exports?
There is no single best chunk size for every application. The right value depends on the size of each record, query complexity, available memory, and whether the export runs through a queue. Start with a reasonable value, such as the example's 1000 records, then measure performance and adjust.
Why does Laravel Excel still run out of memory with FromQuery?
FromQuery reduces the amount of database data retrieved at once, but XLSX generation still requires memory for spreadsheet processing. Large cell contents, styling, formulas, relationships, or other workbook operations can increase memory usage. Try reducing chunk size and unnecessary processing; consider CSV for very large, mostly unformatted datasets.
Can I use WithChunkReading to export records?
No. WithChunkReading is intended for importing large spreadsheets into Laravel. For database exports, use FromQuery; if you need to control the export chunk size, use WithCustomChunkSize. Keeping import and export concerns separate prevents one of the most common implementation mistakes.
How do I export millions of records without a browser timeout?
Use a queued FromQuery export so that the HTTP request only starts the background work. Ensure that queue workers, temporary storage, and completion handling are configured correctly. For extremely large datasets, a CSV export or splitting the output into multiple files may be more practical than one XLSX workbook. If your goal is to export large data with chunk in laravel-excel, start with FromQuery and queueing, then choose the output format based on measured memory usage and actual reporting requirements.
Originally published at headsntail.com.
Top comments (0)