# 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://muhammetsafak.com/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.

## Short answer

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. I covered the decision to get files off the app's own disk in [the local-disk-to-S3 question](/just-ask/moving-local-files-to-s3-is-efs-a-good-interim-step/).

## Why

1. **The memory limit isn't a ceiling, it's a consequence of the design.** Raising `memory_limit` turns a 5 GB file into a 5 GB RAM problem; until the model changes, the limit keeps coming back.

2. **Intermediate layers cancel the stream silently.** Even with correct PHP, if FastCGI/Nginx buffering is on the file re-accumulates in memory.

3. **The most scalable path is taking the app out of the data path.** PHP authorizes, the web server or object storage carries the bytes.

## What to do

1. **Best option: keep the bytes out of the app entirely.** If the file lives in S3/object storage, mint a short-lived `presigned URL` and let the user download directly. If it's on disk, let the web server serve it via `X-Accel-Redirect` (Nginx) or `X-Sendfile` (Apache).

2. **If it must go through PHP, stream it chunk-by-chunk.** Open the file with `Storage::readStream()`, return a `StreamedResponse` (or `response()->streamDownload`), and inside the callback loop on `fread`, write, and call `flush()`. Set `Content-Length` and `Content-Disposition`.

3. **Disable output buffering for that route.** PHP's `ob_*` and Nginx's `fastcgi_buffering`/`proxy_buffering` must be off; the `X-Accel-Buffering: no` header tells Nginx "don't buffer this."

4. **With Octane, don't hold the response in memory.** In a persistent process, accumulating a giant response body in the worker means memory isn't reclaimed between requests. Stream straight to the output.

**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://muhammetsafak.com/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://muhammetsafak.com/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://muhammetsafak.com/just-ask/should-i-put-my-api-behind-a-cloudflare-tunnel/) — Just Ask
