# How do I stream 2-5 GB file downloads without blowing up PHP's memory?

> Serve 2-5 GB files outside PHP via a presigned URL or X-Accel-Redirect; if it must go through PHP, chunk it with readStream and turn off buffering.

- Asked: 2026-05-22
- Answered: 2026-05-25
- Asked by: Barış
- Tags: performans, laravel, altyapi
- Source: https://www.muhammetsafak.com.tr/en/just-ask/streaming-multi-gigabyte-file-downloads-without-exhausting-php-memory/
- Language: en-US
- Author: Muhammet Şafak

---
**Question:** Users download 2-5 GB log archives as ZIPs. When I try to read the file with `file_get_contents()` or `Storage::get()` in PHP and return it, `memory_limit` is exceeded and the request blows up.

How do I build something that pins memory at a few megabytes, reads the file from disk chunk-by-chunk, and pushes it straight to the HTTP response stream?


Short answer: `file_get_contents`/`Storage::get` pulls the WHOLE file into memory; for 2-5 GB you need to stream it — but better still, never pipe the bytes through PHP at all.

The problem is conceptual: you're treating an HTTP response as "build it all, then send it." For a large file the right model is "read-emit-read-emit"; you transfer as you produce, and memory stays flat.

1. **If it must go through PHP, stream it chunk-by-chunk.** In Laravel, open the file with `Storage::readStream()`, return a `StreamedResponse` (or `response()->streamDownload`), and inside the callback loop on `fread`, write, and call `flush()`. That way only one chunk (a few MB) sits in memory at a time. Set `Content-Length` and `Content-Disposition` so the browser shows the download dialog and a correct progress bar.
2. **Disable output buffering for that route.** This is the real trap: if PHP's `ob_*` or Nginx/FastCGI's `fastcgi_buffering`/`proxy_buffering` is on, the layer re-accumulates the file in memory and cancels your stream. Turn buffering off on that endpoint; the `X-Accel-Buffering: no` header tells Nginx "don't buffer this."
3. **With Octane, don't hold the response in memory.** In a persistent process (Octane/Swoole), accumulating a giant response body in the worker means memory isn't reclaimed between requests and the process bloats. Stream straight to the output, don't stash it in an intermediate variable.
4. **The best option: keep the bytes out of the app entirely.** At scale, the right answer is to take PHP out of the data path. If the file lives in S3/object storage, mint a short-lived `presigned URL` and let the user download directly from there. If it's on disk, let the web server serve it via `X-Accel-Redirect` (Nginx) or `X-Sendfile` (Apache); the app only checks authorization and returns the header.

**Bottom line:** I'd reach for a presigned URL or `X-Sendfile`/`X-Accel-Redirect` first — PHP authorizes, it doesn't carry the bytes; cheapest and most scalable. If you genuinely have to serve through PHP, chunk it with `readStream` + `StreamedResponse` and don't forget to disable output buffering. One rule: never load 5 GB into memory at once — not in PHP, not in the layer in front of it.

## Related Reading

- [Should I use chunkById instead of chunk when the same job also updates the rows it iterates?](https://www.muhammetsafak.com.tr/en/just-ask/should-i-use-chunkbyid-instead-of-chunk-when-the-same-job/) — Just Ask
- [Should I enable Model::preventLazyLoading only locally or in production too?](https://www.muhammetsafak.com.tr/en/just-ask/should-i-enable-model-preventlazyloading-only-locally-or-in-production-too/) — Just Ask
- [Should I put my API server behind a Cloudflare Tunnel and close port 443 to the internet?](https://www.muhammetsafak.com.tr/en/just-ask/should-i-put-my-api-behind-a-cloudflare-tunnel/) — Just Ask
