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.

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.

Tags: #performance#laravel#infrastructure
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