Skip to content
Muhammet Şafak
tr
Asked by: Barış Answered:

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


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?

Answer

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.

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.

Share:

Comments

Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.

More Questions

All questions

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind