Why Laravel Excel eats gigabytes while fast-excel-laravel uses tens of megabytes
A scenario familiar to almost anyone who has exported reports to Excel from Laravel: everything flies on
demo data, and then in production, at the end of the month, the export dies with
Allowed memory size of 536870912 bytes exhausted — or hangs so long that it hits max_execution_time.
The first reaction is to raise memory_limit to a gigabyte. That helps until the data grows again.
The problem is not in your code. It is in the architecture of the engine behind the most popular Excel
package for Laravel, maatwebsite/excel. Let's look at where the pain comes from and why
avadim/fast-excel-laravel (version 4.x) is free of it by construction.
The root of the problem: “the whole workbook in memory”
maatwebsite/excel is a thin and convenient wrapper around PhpSpreadsheet. And PhpSpreadsheet works like
this: it builds a complete object model of the entire workbook in RAM. Every cell is a PHP object with
a value, a type, a style, and coordinates. Until the file is written to disk, all of those objects live in
memory at once.
For a document editor that is the right decision: to change A1, then Z100, then A1 again, you need
random access to every cell. But for the typical task of “dump 200,000 rows from the database into Excel”
you pay for that freedom with memory that grows linearly with the number of cells. The package's own
documentation acknowledges it — PhpSpreadsheet keeps the values of all cells in memory, which causes
trouble on large exports.
Why chunking does not save you
The standard advice is “read the data from the database in chunks” (FromQuery, chunk()). That reduces
memory on the database side: you do not pull the entire result set into an Eloquent collection at once.
But it does not touch the other half of the problem: the Excel document itself is still assembled in
memory in full before it is written. You fed the engine data in portions — and it still accumulated the
complete object tree of the workbook out of them.
Hence the common complaint: “I already switched to chunks, and memory still runs out.” Chunking the query and streaming the file are two different things, and without the second one the first does not solve the original problem.
How streaming export works
avadim/fast-excel-laravel is a wrapper around the pair avadim/fast-excel-writer and
avadim/fast-excel-reader, which write and read XLSX as a stream. There are two key mechanisms, and
they work together.
On the file side — forward-only writing. A written row is flushed to a temporary file immediately and leaves memory. At any moment memory holds roughly one current row, not the whole workbook. Memory does not grow with the number of rows.
On the data side — reading through cursor(). Internally, exportModel() walks the model with an
Eloquent cursor:
// simplified — what exportModel() does under the hood
foreach ($model::cursor() as $record) {
yield $record;
}
cursor() keeps one model in memory at a time and never materializes the whole result set. As a result
both ends of the pipeline — reading from the database and writing to the file — work as streams, and
memory stays flat regardless of the volume.
According to the benchmark in the README of the underlying FastExcelWriter, on an export of comparable size streaming writing uses an order of magnitude less memory than the “whole workbook in memory” model, and is noticeably faster. The exact numbers depend on the data and the environment, but something else matters more: memory use stops depending on the number of rows.
In practice: an export that does not fall over
The most common case is exporting an entire model. A few lines are enough:
// The \Excel facade is registered automatically (package auto-discovery)
public function export()
{
$excel = \Excel::create('Users');
// Headings in the first row plus styles, then a streaming export of the whole model
$excel->sheet()
->withHeadings()
->applyFontStyleBold()
->applyBorder('thin')
->exportModel(User::class);
// Send the file to the browser; the temporary file is removed after sending
return $excel->download('users.xlsx');
}
exportModel(User::class) walks the entire table with a cursor and writes the rows one after another. Ten
thousand rows or two million — peak memory will be of the same order.
If you need not the whole model but an arbitrary query or a transformation, there is writeData(), which
accepts an array, a collection, or a generator. A generator is what you want for large volumes, because
it too yields rows one at a time:
$excel = \Excel::create('Orders');
$sheet = $excel->sheet();
$sheet->writeData(function () {
foreach (Order::where('paid', true)->lazy() as $order) {
yield [
'id' => $order->id,
'date' => $order->created_at,
'amount' => $order->amount,
];
}
});
$excel->saveTo('exports/orders.xlsx'); // relative to storage_path()
The key point: do not call ->get() or ->all() before exporting — that materializes the whole result set
in memory and defeats the entire purpose. Use cursor()/lazy(), or exportModel() directly.
Migration table: from Laravel Excel to fast-excel-laravel
If you are porting an existing export, here is how the typical operations map. The methods in the right
column are real ones from the fast-excel-laravel 4.x API.
| Task | Laravel Excel (before) | fast-excel-laravel (after) |
|---|---|---|
| Export class | a separate class implements FromCollection |
not needed — a direct call in the controller or service |
| Create a workbook | Excel::download(new UsersExport, 'u.xlsx') |
$excel = \Excel::create('Users'); |
| Export a model | FromQuery + query() |
$excel->sheet()->exportModel(User::class); |
| Export a result set | FromCollection + collection() |
$sheet->writeData($collection); or a generator |
| Headings | WithHeadings + headings() |
->withHeadings() or ->withHeadings(['A', 'B']) |
| Row transformation | WithMapping + map() |
->mapping(fn($m) => [...]) |
| Column formats | WithColumnFormatting |
->formatAttributes(['price' => '#,##0.00']) |
| Download in the browser | Excel::download(...) |
$excel->download('users.xlsx'); |
| Store on a disk | Excel::store(new Export, 'file.xlsx', 's3') |
$excel->store('s3', 'path/file.xlsx'); |
Note the first row: there is no separate export class with a set of interfaces here. The export logic is a few fluent calls right where you need them.
Honest boundaries
fast-excel-laravel is a specialized tool, not a drop-in replacement for PhpSpreadsheet in every scenario.
What to know before switching:
- Writing is XLSX only. No XLS, no ODS, no CSV on the write side.
\Excel::create()always produces XLSX. (Reading is broader: XLSX and legacy XLS, with the format detected from the file signature.) - Forward-only writing. You cannot write a row and then go back to fix a cell that has already been
flushed. Reports that are built top to bottom (header → data → totals) — which is nearly every real
export — port over easily. For local random access there are areas (
beginArea()), which are buffered as a whole. - Formulas are not evaluated in PHP. The library writes the formula text, and Excel computes it when the file is opened. If you need the resulting value inside the file, compute it in PHP.
- No built-in import validation or queueing out of the box. Validating rows and pushing a heavy import onto a queue is something you arrange yourself with Laravel's own tools.
The rule is simple: PhpSpreadsheet is a document editor, fast-excel-laravel is a fast generator and importer of reports. If your task is to open a complex existing file, tweak individual cells in it, and save — stay on PhpSpreadsheet: that is a tool for a different job.
Who this suits
The switch is justified if at least one of these is about you:
- the export runs out of memory or hits a timeout on real-world volumes;
- the reports keep growing, and you have already raised
memory_limit“for the future”; - the export comes from an Eloquent model or a large query and is built top to bottom;
- you would like to trade the infrastructure of export classes for a few readable calls.
But a stable fifty-row export that has worked for ages and bothers no one is not worth rewriting.
Summary
- Out-of-memory failures on large exports in Laravel Excel follow from PhpSpreadsheet's “whole workbook in memory” architecture, not from your code.
- Chunking on the database side does not solve it: the document is still assembled in memory in full.
fast-excel-laravelwrites as a stream and reads data throughcursor()— memory stays flat regardless of the number of rows.- The price is specialization: XLSX writing only, forward-only writing, formulas computed by Excel. For generating reports that is exactly what you want.
composer require avadim/fast-excel-laravel
The repository is github.com/aVadim483/fast-excel-laravel.