For a Laravel sales report of a million rows, when do you stream it in the request, and when do you generate it in a queued job instead?
answer
- who waits, and for how long
- a stream holds a worker and a connection
- max_execution_time and proxy timeouts
- failure after 200 means a truncated file
- job stores the file, then notifies
basics
~20 sStream with streamDownload() when the export finishes within about a minute and the user waits for it. Queue a job that writes the file to a disk and sends a link when it runs longer, must survive failures, or many users export at once.
solid answer
~50 sA streamed export holds one PHP worker and one open connection for its whole duration, is cut off by PHP's `max_execution_time` (30 seconds in the shipped php.ini files unless raised) and any load-balancer or proxy idle timeout, and on failure leaves the user with a truncated file under a 200 status. That is fine for exports that finish quickly and are cheap to retry by clicking again. When the report takes minutes, is requested by many users at once, or must be complete and auditable, dispatch a queued job instead: it writes the CSV to a Storage disk, can retry on failure, and tells the user through a notification or email with a download link. A progress bar can come from the job updating a cache key and an `eventStream()` or polling endpoint reading it. Many apps cap synchronous exports by row count and queue the rest.
code
php · 25 lines<?php
use App\Jobs\GenerateSalesExport;
use App\Models\Sale;
use Illuminate\Http\Request;
class SalesExportController
{
public function __invoke(Request $request)
{
$query = Sale::query()->whereYear('sold_at', $request->integer('year'));
if ($query->count() <= 50_000) {
return response()->streamDownload(
fn () => $this->writeCsv($query),
'sales.csv',
['Content-Type' => 'text/csv'],
);
}
GenerateSalesExport::dispatch($request->user(), $request->integer('year'));
return back()->with('status', 'Your export is being prepared; we will email you a link.');
}
}go deeper
Know both options: streamDownload() for exports that finish quickly, and a queued job plus a link for long ones.
Explain what a stream occupies (a worker, a connection) and what limits it (max_execution_time, proxy timeouts), and what a job needs (a disk, a queue worker, a notification).
Choose by duration, concurrency and cost of truncation; combine a queued job with a progress feed; and diagnose timeout cut-offs across PHP, web server and load balancer.
Set an export policy with thresholds, dedicated queue capacity and retention for generated files, so reporting load never competes with customer traffic.
## Two ways to deliver the same file The finance team's **Export sales** button can be served in two ways: 1. **In the request:** `response()->streamDownload()` writes rows to the client as they are read. 2. **In the background:** the controller dispatches a queued job and returns immediately; the job writes `sales-2026.csv` to a disk, and the user gets a link when it is done. Both avoid holding a million rows in memory. They differ in who waits, what fails, and what capacity they consume. Dispatching and running jobs are the queue system's subject; here the question is the choice. ## What streaming costs - **A worker per export.** Under PHP-FPM each request occupies one worker process until the callback ends; under Octane it occupies a worker too. Ten people exporting for two minutes each take ten workers away from ordinary page views for two minutes. - **Time limits on every hop.** PHP's `max_execution_time` is 30 in the shipped `php.ini-production` and `php.ini-development` files, and the web server, reverse proxy and load balancer each have their own timeouts, often counted as idle time between bytes. An export that pauses on a slow query can be cut off by any of them. - **No second chance after the first byte.** Once the 200 status is sent, a failure produces a truncated file. `streamDownload()` reports the error through `StreamedResponseException`, but the user already has a file that looks plausible. - **No resumption.** A dropped connection at 90% means starting again from zero. ## What a queued job costs - **More moving parts:** a queue worker, a disk to store the file, cleanup of old files, and a way to tell the user it is ready. - **A different user experience:** "We'll email you when your export is ready" instead of an immediate download. - **Worker timeouts of its own:** long jobs need their timeout and retry settings sized for the export, which is queue configuration. In exchange the job can **retry** after a transient failure, writes a **complete** file or none, can run on dedicated workers so web traffic is unaffected, and leaves an artefact you can audit or re-download. ## A decision table | Signal | Stream in the request | Queue a job | |---|---|---| | Typical duration | seconds to about a minute | minutes or more | | Concurrent exporters | few | many, or unpredictable | | Cost of a truncated file | low, user clicks again | high, finance or compliance data | | Needs retries or an audit trail | no | yes | | Infrastructure available | web workers only | queue workers and a storage disk | | User expectation | immediate download | notification or email link | ## Combining the two The two tools also work together: - **Cap and queue:** stream exports up to, say, 50,000 rows or one month, and queue anything larger. - **Queue and watch:** dispatch the job, have it update a cache key such as `export:{id}` with rows done, and let the page open an `eventStream()` feed that yields progress and ends when the job finishes. The feed holds a worker too, so keep it short-lived and cheap, and fall back to polling if connections are scarce. - **Queue and download:** when the job finishes, the user downloads the stored file with an ordinary file response or a temporary URL from the disk, which no longer depends on the export's duration. ## Operating either option Whichever path you choose, make it observable: - Log the start and end of each export with its row count and duration, so you can see when synchronous exports approach the timeout limits. - For queued exports, keep generated files for a limited time and delete them on a schedule, since finance data should not accumulate on a disk forever. - Put export jobs on their own queue so a burst of large reports does not delay password-reset emails or payment webhooks. - Revisit the threshold as the data grows: an export that streamed comfortably at launch may not a year later. ## What a senior answer shows It names the concrete costs of streaming (worker occupancy, timeouts, truncation after a 200, no resume), the benefits of a job (retries, completeness, isolation), and a threshold-based policy rather than one answer for every export. It also keeps the progress feed and the file delivery as separate, cheap requests.
- The streamed export works locally but users on production get files that stop after about 60 seconds. Where do you look?At every timeout between PHP and the browser: PHP's `max_execution_time` and the PHP-FPM request timeout, the web server's upstream read timeout, and the load balancer's idle timeout. A cut-off at a round number usually points to one of them. Raising limits helps briefly; moving long exports to a queued job removes the dependency.
- How would you show a progress bar for the queued version?Have the job write progress, such as rows done and total, to a cache key named after the export, and expose a small endpoint that reads it: either polled every few seconds or an `eventStream()` feed that yields a `StreamedEvent` per update and finishes when the job marks the export complete.
Streaming is a print shop that runs your order while you stand at the counter: fine for a short job, but you hold a counter spot the whole time and walk away with whatever was printed if the machine jams. A queued job is leaving your number and being called when the complete order is boxed and ready.
saying these in an interview costs you the question
- Streaming is always better because it uses no memory.
- A streamed export can be resumed after the connection drops.
- Raising max_execution_time alone makes long streamed exports reliable.
- A queued export blocks the web worker until the file is ready.
- If a streamed export fails, the user always sees an error page.